Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/pull-request-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ name: "Test Pull Request"

on:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.20'
go-version: '1.25'
- run: go get -t -v ./...
- run: go test -v -race ./...
6 changes: 3 additions & 3 deletions .github/workflows/tag-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.20'
go-version: '1.25'
- run: go get -t -v ./...
- run: go test -v -race ./...

Expand Down
198 changes: 153 additions & 45 deletions DefaultEnv_plugin.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"net/http"
"net/url"
"os"
"strings"

"code.cloudfoundry.org/cli/plugin"
cfclient "github.com/cloudfoundry/go-cfclient/v3/client"
cfconfig "github.com/cloudfoundry/go-cfclient/v3/config"
"github.com/cloudfoundry/go-cfclient/v3/resource"
)

// DefaultEnvPlugin allows users to export environment variables of an app into a JSON file
Expand All @@ -22,7 +28,7 @@ func (*DefaultEnvPlugin) Run(cliConnection plugin.CliConnection, args []string)
return
}
if err := runDefaultEnv(cliConnection, args); err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
_, _ = fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
Expand All @@ -31,9 +37,9 @@ func (c *DefaultEnvPlugin) GetMetadata() plugin.PluginMetadata {
return plugin.PluginMetadata{
Name: "DefaultEnv",
Version: plugin.VersionType{
Major: 1,
Minor: 1,
Build: 1,
Major: 2,
Minor: 0,
Build: 0,
},
MinCliVersion: plugin.VersionType{
Major: 7,
Expand All @@ -47,6 +53,11 @@ func (c *DefaultEnvPlugin) GetMetadata() plugin.PluginMetadata {
HelpText: "Create default-env.json file with environment variables of an app.",
UsageDetails: plugin.Usage{
Usage: "cf default-env APP",
Options: map[string]string{
"f": "output file name (default: default-env.json)",
"guid": "specify the app GUID directly instead of app name",
"stdout": "write output to stdout instead of a file",
},
},
},
},
Expand All @@ -57,76 +68,173 @@ func main() {
plugin.Start(new(DefaultEnvPlugin))
}

// environmentResponse from /v3/apps/:guid/env
type environmentResponse struct {
SystemEnvJson map[string]interface{} `json:"system_env_json"`
ApplicationEnvJson map[string]interface{} `json:"application_env_json"`
EnvironmentVariables map[string]interface{} `json:"environment_variables"`
}
// createClient creates a new CF client using the access token and API endpoint from the CLI connection
func createClient(connection plugin.CliConnection) (*cfclient.Client, error) {
accessToken, err := connection.AccessToken()
if err != nil {
return nil, fmt.Errorf("failed to retrieve access token: %w", err)
}

// Merge all environment variables into one map
func (e environmentResponse) Merge() map[string]interface{} {
content := make(map[string]interface{})
for k, v := range e.SystemEnvJson {
content[k] = v
endpoint, err := connection.ApiEndpoint()
if err != nil {
return nil, fmt.Errorf("failed to retrieve API endpoint: %w", err)
}
for k, v := range e.ApplicationEnvJson {
content[k] = v

cfg, err := cfconfig.New(endpoint, cfconfig.Token(accessToken, ""))
if err != nil {
return nil, fmt.Errorf("failed to create CF client config: %w", err)
}
for k, v := range e.EnvironmentVariables {
content[k] = v

client, err := cfclient.New(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create CF client: %w", err)
}
return content

return client, nil
}

// marshalAndWrite marshals v (any) into JSON and writes it to a file
func marshalAndWrite(v interface{}, filename string) error {
f, err := os.Create(filename)
// findAppByName retrieves the app with the specified name in the given space
func findAppByName(ctx context.Context, client *cfclient.Client, appName, spaceGUID string) (*resource.App, error) {
app, err := client.Applications.Single(ctx, &cfclient.AppListOptions{
Names: cfclient.Filter{Values: []string{appName}},
SpaceGUIDs: cfclient.Filter{Values: []string{spaceGUID}},
})
if err != nil {
return err
return nil, fmt.Errorf("failed to retrieve app: %w", err)
}
defer func(f *os.File) {
_ = f.Close()
}(f)
return app, nil
}

data, err := json.Marshal(v)
func findEnvironmentByAppGUID(ctx context.Context, client *cfclient.Client, appGUID string) (*AppEnvironment, error) {
seg := url.PathEscape(appGUID)
p, err := url.JoinPath("/v3/apps", seg, "env")
if err != nil {
return err
return nil, fmt.Errorf("failed to construct API path: %w", err)
}

if _, err = f.Write(data); err != nil {
return err
var environment AppEnvironment
req, err := http.NewRequestWithContext(ctx, "GET", client.ApiURL(p), nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := client.ExecuteAuthRequest(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()

return nil
if err := json.NewDecoder(resp.Body).Decode(&environment); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &environment, nil
}

// runDefaultEnv fetches and merges the environment variables of a specified CF app into a JSON file
func runDefaultEnv(cliConnection plugin.CliConnection, args []string) error {
if len(args) != 2 {
return ErrAppNotSpecified
fs := flag.NewFlagSet("cf-defaultenv-plugin", flag.ExitOnError)
outFileFlag := fs.String("f", "default-env.json", "output file name")
appGUIDFlag := fs.String("guid", "", "specify the app GUID directly instead of app name")
writeStdoutFlag := fs.Bool("stdout", false, "write output to stdout instead of a file")
if err := fs.Parse(args[1:]); err != nil {
return fmt.Errorf("failed to parse flags: %w", err)
}

app, err := cliConnection.GetApp(args[1])
ctx := context.Background()

client, err := createClient(cliConnection)
if err != nil {
return err
}

url := fmt.Sprintf("/v3/apps/%s/env", app.Guid)
env, err := cliConnection.CliCommandWithoutTerminalOutput("curl", url)
currentSpace, err := cliConnection.GetCurrentSpace()
if err != nil {
return err
return fmt.Errorf("failed to retrieve current space: %w", err)
}

var data environmentResponse
if err = json.Unmarshal([]byte(strings.Join(env, "")), &data); err != nil {
return err
var appGUID string
if *appGUIDFlag != "" {
// if the user specified the app GUID directly, use it instead of looking up the app by name
appGUID = *appGUIDFlag

_, _ = fmt.Fprintln(os.Stderr, "info: using provided app GUID", appGUID)
} else {
appName := fs.Arg(0)
if appName == "" {
_, _ = fmt.Fprintln(os.Stderr, "error: app name or -guid flag must be specified")
fs.Usage()
return nil
}

app, err := findAppByName(ctx, client, appName, currentSpace.Guid)
if err != nil {
return err
}
appGUID = app.GUID

_, _ = fmt.Fprintln(os.Stderr, "info: retrieved app", appName, "with GUID", appGUID)
}

if err = marshalAndWrite(data.Merge(), "default-env.json"); err != nil {
return err
env, err := findEnvironmentByAppGUID(ctx, client, appGUID)
if err != nil {
return fmt.Errorf("failed to retrieve app environment: %w", err)
}

fmt.Println("Environment variables for " + args[1] + " written to default-env.json")
result := Merge(env.SystemEnvVars, env.AppEnvVars, env.EnvVars)

if *writeStdoutFlag {
if err := marshalAndWriteStdout(result); err != nil {
return err
}
_, _ = fmt.Fprintln(os.Stderr, "success: environment variables written to stdout")
} else {
if err := marshalAndWrite(result, *outFileFlag); err != nil {
return err
}
_, _ = fmt.Fprintln(os.Stderr, "success: environment variables written to", *outFileFlag)
}
return nil
}

// AppEnvironment is a stripped down version of [cfclient.Environment]
// that only contains the fields we care about for this plugin with a slightly different structure to make it easier
// to merge into a single map.
type AppEnvironment struct {
EnvVars map[string]any `json:"environment_variables,omitempty"`
SystemEnvVars map[string]any `json:"system_env_json,omitempty"` // VCAP_SERVICES
AppEnvVars map[string]any `json:"application_env_json,omitempty"` // VCAP_APPLICATION
}

// Merge merges multiple maps into a single map
func Merge[Map ~map[K]V, K comparable, V any](maps ...Map) Map {
result := make(Map)
for _, m := range maps {
for k, v := range m {
result[k] = v
}
}
return result
}

// marshalAndWrite marshals v (any) into JSON and writes it to a file
func marshalAndWrite(v any, filename string) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer func(f *os.File) {
_ = f.Close()
}(f)
if err := json.NewEncoder(f).Encode(v); err != nil {
return err
}
return nil
}

func marshalAndWriteStdout(v any) error {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(v)
}
18 changes: 17 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
module github.com/saphanaacademy/DefaultEnv

go 1.20
go 1.25

require (
code.cloudfoundry.org/cli v7.1.0+incompatible
github.com/cloudfoundry/go-cfclient/v3 v3.0.0-alpha.17
)

require (
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab // indirect
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.39.1 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading