Skip to content

Commit

Permalink
feat: support --inline flag in 'argocd admin app/proj generate-spec' …
Browse files Browse the repository at this point in the history
…commands (argoproj#6804)



Signed-off-by: Alexander Matyushentsev <[email protected]>
  • Loading branch information
Alexander Matyushentsev authored Jul 26, 2021
1 parent 75a5f79 commit fb357de
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 101 deletions.
11 changes: 8 additions & 3 deletions cmd/argocd/commands/admin/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/argoproj/argo-cd/v2/util/config"
"github.com/argoproj/argo-cd/v2/util/db"
"github.com/argoproj/argo-cd/v2/util/errors"
"github.com/argoproj/argo-cd/v2/util/io"
kubeutil "github.com/argoproj/argo-cd/v2/util/kube"
"github.com/argoproj/argo-cd/v2/util/settings"
)
Expand Down Expand Up @@ -62,6 +63,7 @@ func NewGenAppSpecCommand() *cobra.Command {
labels []string
outputFormat string
annotations []string
inline bool
)
var command = &cobra.Command{
Use: "generate-spec APPNAME",
Expand Down Expand Up @@ -94,16 +96,19 @@ func NewGenAppSpecCommand() *cobra.Command {
os.Exit(1)
}

var printResources []interface{}
printResources = append(printResources, app)
errors.CheckError(cmdutil.PrintResources(printResources, outputFormat))
out, closer, err := getOutWriter(inline, fileURL)
errors.CheckError(err)
defer io.Close(closer)

errors.CheckError(PrintResources(outputFormat, out, app))
},
}
command.Flags().StringVar(&appName, "name", "", "A name for the app, ignored if a file is set (DEPRECATED)")
command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the app")
command.Flags().StringArrayVarP(&labels, "label", "l", []string{}, "Labels to apply to the app")
command.Flags().StringArrayVarP(&annotations, "annotations", "", []string{}, "Set metadata annotations (e.g. example=value)")
command.Flags().StringVarP(&outputFormat, "output", "o", "yaml", "Output format. One of: json|yaml")
command.Flags().BoolVarP(&inline, "inline", "i", false, "If set then generated resource is written back to the file specified in --file flag")

// Only complete files with appropriate extension.
err := command.Flags().SetAnnotation("file", cobra.BashCompFilenameExt, []string{"json", "yaml", "yml"})
Expand Down
6 changes: 2 additions & 4 deletions cmd/argocd/commands/admin/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,10 +581,8 @@ func NewGenClusterConfigCommand(pathOpts *clientcmd.PathOptions) *cobra.Command
secret, err := kubeClientset.CoreV1().Secrets(ArgoCDNamespace).Get(context.Background(), secName, v1.GetOptions{})
errors.CheckError(err)

cmdutil.ConvertSecretData(secret)
var printResources []interface{}
printResources = append(printResources, secret)
errors.CheckError(cmdutil.PrintResources(printResources, outputFormat))
ConvertSecretData(secret)
errors.CheckError(PrintResources(outputFormat, os.Stdout, secret))
},
}
command.PersistentFlags().StringVar(&pathOpts.LoadingRules.ExplicitPath, pathOpts.ExplicitFileFlag, pathOpts.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
Expand Down
105 changes: 105 additions & 0 deletions cmd/argocd/commands/admin/generatespec_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package admin

import (
"encoding/json"
"errors"
"fmt"
"io"
"os"

"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/ghodss/yaml"
v1 "k8s.io/api/core/v1"

ioutil "github.com/argoproj/argo-cd/v2/util/io"
)

func getOutWriter(inline bool, filePath string) (io.Writer, io.Closer, error) {
if !inline {
return os.Stdout, ioutil.NopCloser, nil
}

if filePath == "" {
return nil, nil, errors.New("The file path must be specified using flag '--file'")
}

err := os.Rename(filePath, fmt.Sprintf("%s.back", filePath))
if err != nil {
return nil, nil, err
}

fileOut, err := os.Create(filePath)
if err != nil {
return nil, nil, err
}
return fileOut, fileOut, nil
}

// PrintResources prints a single resource in YAML or JSON format to stdout according to the output format
func PrintResources(output string, out io.Writer, resources ...interface{}) error {
for i, resource := range resources {
filteredResource, err := omitFields(resource)
if err != nil {
return err
}
resources[i] = filteredResource
}
var obj interface{} = resources
if len(resources) == 1 {
obj = resources[0]
}

switch output {
case "json":
jsonBytes, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return err
}

_, _ = fmt.Fprintln(out, string(jsonBytes))
case "yaml":
yamlBytes, err := yaml.Marshal(obj)
if err != nil {
return err
}
// marshaled YAML already ends with the new line character
_, _ = fmt.Fprint(out, string(yamlBytes))
default:
return fmt.Errorf("unknown output format: %s", output)
}
return nil
}

// omit fields such as status, creationTimestamp and metadata.namespace in k8s objects
func omitFields(resource interface{}) (interface{}, error) {
jsonBytes, err := json.Marshal(resource)
if err != nil {
return nil, err
}

toMap := make(map[string]interface{})
err = json.Unmarshal([]byte(string(jsonBytes)), &toMap)
if err != nil {
return nil, err
}

delete(toMap, "status")
if v, ok := toMap["metadata"]; ok {
if metadata, ok := v.(map[string]interface{}); ok {
delete(metadata, "creationTimestamp")
delete(metadata, "namespace")
}
}
return toMap, nil
}

// ConvertSecretData converts kubernetes secret's data to stringData
func ConvertSecretData(secret *v1.Secret) {
secret.Kind = kube.SecretKind
secret.APIVersion = "v1"
secret.StringData = map[string]string{}
for k, v := range secret.Data {
secret.StringData[k] = string(v)
}
secret.Data = map[string][]byte{}
}
38 changes: 38 additions & 0 deletions cmd/argocd/commands/admin/generatespec_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package admin

import (
"fmt"
"io/ioutil"
"os"
"testing"

"github.com/argoproj/argo-cd/v2/util/io"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetOutWriter_InlineOff(t *testing.T) {
out, closer, err := getOutWriter(false, "")
require.NoError(t, err)
defer io.Close(closer)

assert.Equal(t, os.Stdout, out)
}

func TestGetOutWriter_InlineOn(t *testing.T) {
tmpFile, err := ioutil.TempFile("", "")
require.NoError(t, err)
defer func() {
_ = os.Remove(tmpFile.Name())
_ = os.Remove(fmt.Sprintf("%s.back", tmpFile.Name()))
}()

out, closer, err := getOutWriter(true, tmpFile.Name())
require.NoError(t, err)
defer io.Close(closer)

assert.Equal(t, tmpFile.Name(), out.(*os.File).Name())
_, err = os.Stat(fmt.Sprintf("%s.back", tmpFile.Name()))
assert.NoError(t, err, "Back file must be created")
}
13 changes: 9 additions & 4 deletions cmd/argocd/commands/admin/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
appclient "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/typed/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/cli"
"github.com/argoproj/argo-cd/v2/util/errors"
"github.com/argoproj/argo-cd/v2/util/io"

"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/spf13/cobra"
Expand All @@ -35,12 +36,13 @@ func NewProjectsCommand() *cobra.Command {
return command
}

// NewGenProjectConfigCommand generates declarative configuration file for given project
// NewGenProjectSpecCommand generates declarative configuration file for given project
func NewGenProjectSpecCommand() *cobra.Command {
var (
opts cmdutil.ProjectOpts
fileURL string
outputFormat string
inline bool
)
var command = &cobra.Command{
Use: "generate-spec PROJECT",
Expand All @@ -49,13 +51,16 @@ func NewGenProjectSpecCommand() *cobra.Command {
proj, err := cmdutil.ConstructAppProj(fileURL, args, opts, c)
errors.CheckError(err)

var printResources []interface{}
printResources = append(printResources, proj)
errors.CheckError(cmdutil.PrintResources(printResources, outputFormat))
out, closer, err := getOutWriter(inline, fileURL)
errors.CheckError(err)
defer io.Close(closer)

errors.CheckError(PrintResources(outputFormat, out, proj))
},
}
command.Flags().StringVarP(&outputFormat, "output", "o", "yaml", "Output format. One of: json|yaml")
command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the project")
command.Flags().BoolVarP(&inline, "inline", "i", false, "If set then generated resource is written back to the file specified in --file flag")

// Only complete files with appropriate extension.
err := command.Flags().SetAnnotation("file", cobra.BashCompFilenameExt, []string{"json", "yaml", "yml"})
Expand Down
13 changes: 2 additions & 11 deletions cmd/argocd/commands/admin/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
apiv1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"

Expand Down Expand Up @@ -155,21 +154,13 @@ func NewGenRepoSpecCommand() *cobra.Command {
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, ArgoCDNamespace)
argoDB := db.NewDB(ArgoCDNamespace, settingsMgr, kubeClientset)

var printResources []interface{}
_, err := argoDB.CreateRepository(context.Background(), &repoOpts.Repo)
errors.CheckError(err)

secret, err := kubeClientset.CoreV1().Secrets(ArgoCDNamespace).Get(context.Background(), db.RepoURLToSecretName(repoSecretPrefix, repoOpts.Repo.Repo), v1.GetOptions{})
if err != nil {
if !apierr.IsNotFound(err) {
errors.CheckError(err)
}
} else {
cmdutil.ConvertSecretData(secret)
printResources = append(printResources, secret)
}
errors.CheckError(err)

errors.CheckError(cmdutil.PrintResources(printResources, outputFormat))
errors.CheckError(PrintResources(outputFormat, os.Stdout, secret))
},
}
command.Flags().StringVarP(&outputFormat, "output", "o", "yaml", "Output format. One of: json|yaml")
Expand Down
79 changes: 0 additions & 79 deletions cmd/util/common.go
Original file line number Diff line number Diff line change
@@ -1,85 +1,6 @@
package util

import (
"encoding/json"
"fmt"

"github.com/ghodss/yaml"
v1 "k8s.io/api/core/v1"

"github.com/argoproj/gitops-engine/pkg/utils/kube"
)

var (
LogFormat string
LogLevel string
)

// PrintResource prints a single resource in YAML or JSON format to stdout according to the output format
func PrintResources(resources []interface{}, output string) error {
for i, resource := range resources {
filteredResource, err := omitFields(resource)
if err != nil {
return err
}
resources[i] = filteredResource
}
var obj interface{} = resources
if len(resources) == 1 {
obj = resources[0]
}

switch output {
case "json":
jsonBytes, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return err
}

fmt.Println(string(jsonBytes))
case "yaml":
yamlBytes, err := yaml.Marshal(obj)
if err != nil {
return err
}
// marshaled YAML already ends with the new line character
fmt.Print(string(yamlBytes))
default:
return fmt.Errorf("unknown output format: %s", output)
}
return nil
}

// omit fields such as status, creationTimestamp and metadata.namespace in k8s objects
func omitFields(resource interface{}) (interface{}, error) {
jsonBytes, err := json.Marshal(resource)
if err != nil {
return nil, err
}

toMap := make(map[string]interface{})
err = json.Unmarshal([]byte(string(jsonBytes)), &toMap)
if err != nil {
return nil, err
}

delete(toMap, "status")
if v, ok := toMap["metadata"]; ok {
if metadata, ok := v.(map[string]interface{}); ok {
delete(metadata, "creationTimestamp")
delete(metadata, "namespace")
}
}
return toMap, nil
}

// ConvertSecretData converts kubernetes secret's data to stringData
func ConvertSecretData(secret *v1.Secret) {
secret.Kind = kube.SecretKind
secret.APIVersion = "v1"
secret.StringData = map[string]string{}
for k, v := range secret.Data {
secret.StringData[k] = string(v)
}
secret.Data = map[string][]byte{}
}
1 change: 1 addition & 0 deletions docs/user-guide/commands/argocd_admin_app_generate-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ argocd admin app generate-spec APPNAME [flags]
--helm-set-string stringArray Helm set STRING values on the command line (can be repeated to set several values: --helm-set-string key1=val1 --helm-set-string key2=val2)
--helm-version string Helm version
-h, --help help for generate-spec
-i, --inline If set then generated resource is written back to the file specified in --file flag
--jsonnet-ext-var-code stringArray Jsonnet ext var
--jsonnet-ext-var-str stringArray Jsonnet string ext var
--jsonnet-libs stringArray Additional jsonnet libs (prefixed by repoRoot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ argocd admin proj generate-spec PROJECT [flags]
-d, --dest stringArray Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)
-f, --file string Filename or URL to Kubernetes manifests for the project
-h, --help help for generate-spec
-i, --inline If set then generated resource is written back to the file specified in --file flag
--orphaned-resources Enables orphaned resources monitoring
--orphaned-resources-warn Specifies if applications should have a warning condition when orphaned resources detected
-o, --output string Output format. One of: json|yaml (default "yaml")
Expand Down

0 comments on commit fb357de

Please sign in to comment.