From 84a65133d55dd9247e177321af7daaf6e806a86b Mon Sep 17 00:00:00 2001 From: wtschreiter Date: Sun, 24 May 2020 17:54:02 +0200 Subject: [PATCH] Feature ansible modules (#55) Add ansible modules to control rancher * Add dryRun flag for all write operation * Add ansible module cattlectl_apply * Add ansible module cattlectl_list * Add ansible module cattlectl_delete * Document ansible modules * Update changelog --- .travis.yml | 20 ++- CHANGELOG.md | 4 + README.md | 8 + ansible/cattlectl_apply/main.go | 92 ++++++++++ ansible/cattlectl_delete/main.go | 63 +++++++ ansible/cattlectl_list/main.go | 59 +++++++ ansible/utils/util.go | 142 ++++++++++++++++ ansible/utils/values.go | 95 +++++++++++ cmd/apply/apply.go | 7 +- cmd/config.go | 4 + cmd/delete/delete.go | 29 ++-- cmd/list/{lsit.go => list.go} | 15 +- cmd/root.go | 4 + docs/ansible/cattlectl_apply.md | 48 ++++++ docs/ansible/cattlectl_delete.md | 45 +++++ docs/ansible/cattlectl_list.md | 45 +++++ docs/ansible/index.md | 20 +++ docs/{ => cli}/cattlectl.md | 1 + docs/{ => cli}/cattlectl_apply.md | 1 + docs/{ => cli}/cattlectl_completion.md | 1 + docs/{ => cli}/cattlectl_delete.md | 3 +- docs/{ => cli}/cattlectl_gen-doc.md | 1 + docs/{ => cli}/cattlectl_list.md | 3 +- docs/{ => cli}/cattlectl_show.md | 1 + docs/{ => cli}/cattlectl_version.md | 1 + docs/index.md | 2 +- go.mod | 4 - internal/pkg/config/config.go | 81 ++++++++- internal/pkg/ctl/apply.go | 159 +++++++++--------- internal/pkg/ctl/apply_test.go | 10 +- internal/pkg/ctl/delete.go | 78 ++++----- internal/pkg/ctl/list.go | 9 +- internal/pkg/rancher/client/app_client.go | 52 ++++-- .../pkg/rancher/client/app_client_test.go | 4 +- .../pkg/rancher/client/certificate_client.go | 84 +++++---- .../rancher/client/certificate_client_test.go | 4 +- internal/pkg/rancher/client/clients.go | 7 +- .../rancher/client/cluster_catalog_client.go | 42 +++-- .../client/cluster_catalog_client_test.go | 4 +- internal/pkg/rancher/client/cluster_client.go | 7 +- .../pkg/rancher/client/config_map_client.go | 41 +++-- .../rancher/client/config_map_client_test.go | 2 +- internal/pkg/rancher/client/cronjob_client.go | 26 ++- .../pkg/rancher/client/cronjob_client_test.go | 2 +- .../pkg/rancher/client/daemon_set_client.go | 26 ++- .../rancher/client/daemon_set_client_test.go | 2 +- internal/pkg/rancher/client/deployment.go | 26 ++- .../pkg/rancher/client/deployment_test.go | 2 +- .../client/docker_credential_client.go | 83 +++++---- .../client/docker_credential_client_test.go | 4 +- internal/pkg/rancher/client/job_client.go | 37 ++-- .../pkg/rancher/client/job_client_test.go | 2 +- .../pkg/rancher/client/namespace_client.go | 38 +++-- .../rancher/client/namespace_client_test.go | 2 +- .../client/persistent_volume_client.go | 21 ++- .../client/persistent_volume_client_test.go | 2 +- .../rancher/client/project_catalog_client.go | 42 +++-- .../client/project_catalog_client_test.go | 4 +- internal/pkg/rancher/client/project_client.go | 33 ++-- .../pkg/rancher/client/project_client_test.go | 4 +- .../rancher/client/rancher_catalog_client.go | 38 +++-- .../client/rancher_catalog_client_test.go | 6 +- .../pkg/rancher/client/resource_client.go | 27 +-- internal/pkg/rancher/client/secret_client.go | 85 ++++++---- .../pkg/rancher/client/secret_client_test.go | 2 +- .../pkg/rancher/client/stateful_set_client.go | 26 ++- .../client/stateful_set_client_test.go | 2 +- .../rancher/client/storage_class_client.go | 21 ++- .../client/storage_class_client_test.go | 2 +- internal/pkg/rancher/descriptor/converger.go | 54 ++++-- internal/pkg/rancher/model/rancher.go | 28 ++- vendor/modules.txt | 4 - 72 files changed, 1492 insertions(+), 461 deletions(-) create mode 100644 ansible/cattlectl_apply/main.go create mode 100644 ansible/cattlectl_delete/main.go create mode 100644 ansible/cattlectl_list/main.go create mode 100644 ansible/utils/util.go create mode 100644 ansible/utils/values.go rename cmd/list/{lsit.go => list.go} (90%) create mode 100644 docs/ansible/cattlectl_apply.md create mode 100644 docs/ansible/cattlectl_delete.md create mode 100644 docs/ansible/cattlectl_list.md create mode 100644 docs/ansible/index.md rename docs/{ => cli}/cattlectl.md (97%) rename docs/{ => cli}/cattlectl_apply.md (97%) rename docs/{ => cli}/cattlectl_completion.md (96%) rename docs/{ => cli}/cattlectl_delete.md (94%) rename docs/{ => cli}/cattlectl_gen-doc.md (95%) rename docs/{ => cli}/cattlectl_list.md (93%) rename docs/{ => cli}/cattlectl_show.md (96%) rename docs/{ => cli}/cattlectl_version.md (95%) diff --git a/.travis.yml b/.travis.yml index 03b5c88..bec511c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,16 +6,28 @@ env: install: true script: - go test -v -mod=vendor ./... - - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/bitgrip/cattlectl/internal/pkg/ctl.Version=${TRAVIS_TAG:-0.0.0-dev} -d -s -w -extldflags \"-static\"" -a -tags netgo -installsuffix netgo -mod=vendor -o build/linux/cattlectl - - tar czpvf build/cattlectl-${TRAVIS_TAG:-0.0.0-dev}-linux.tar.gz build/linux/ - - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X github.com/bitgrip/cattlectl/internal/pkg/ctl.Version=${TRAVIS_TAG:-0.0.0-dev} -s -w" -a -tags netgo -installsuffix netgo -mod=vendor -o build/darwin/cattlectl - - tar czpvf build/cattlectl-${TRAVIS_TAG:-0.0.0-dev}-darwin.tar.gz build/darwin/ + + - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/bitgrip/cattlectl/internal/pkg/ctl.Version=${TRAVIS_TAG:-0.0.0-dev} -d -s -w -extldflags \"-static\"" -a -tags netgo -installsuffix netgo -mod=vendor -o build/cli/linux/cattlectl + - tar czpvf build/cattlectl-${TRAVIS_TAG:-0.0.0-dev}-linux.tar.gz build/cli/linux/ + + - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X github.com/bitgrip/cattlectl/internal/pkg/ctl.Version=${TRAVIS_TAG:-0.0.0-dev} -s -w" -a -tags netgo -installsuffix netgo -mod=vendor -o build/cli/darwin/cattlectl + - tar czpvf build/cattlectl-${TRAVIS_TAG:-0.0.0-dev}-darwin.tar.gz build/cli/darwin/ + + - mkdir -p build/ansible/linux + - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/bitgrip/cattlectl/internal/pkg/ctl.Version=${TRAVIS_TAG:-0.0.0-dev} -d -s -w -extldflags \"-static\"" -a -tags netgo -installsuffix netgo -mod=vendor -o build/ansible/linux ./ansible/... + - tar czpvf build/cattlectl-ansible-${TRAVIS_TAG:-0.0.0-dev}-linux.tar.gz build/ansible/linux/ + + - mkdir -p build/ansible/darwin + - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X github.com/bitgrip/cattlectl/internal/pkg/ctl.Version=${TRAVIS_TAG:-0.0.0-dev} -s -w" -a -tags netgo -installsuffix netgo -mod=vendor -o build/ansible/darwin ./ansible/... + - tar czpvf build/cattlectl-ansible-${TRAVIS_TAG:-0.0.0-dev}-darwin.tar.gz build/ansible/darwin/ deploy: provider: releases api_key: $GITHUB_API_KEY file: - build/cattlectl-${TRAVIS_TAG:-0.0.0-dev}-linux.tar.gz - build/cattlectl-${TRAVIS_TAG:-0.0.0-dev}-darwin.tar.gz + - build/cattlectl-ansible-${TRAVIS_TAG:-0.0.0-dev}-linux.tar.gz + - build/cattlectl-ansible-${TRAVIS_TAG:-0.0.0-dev}-darwin.tar.gz skip_cleanup: true on: repo: bitgrip/cattlectl diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec1ef7..affb10d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. ### Added +* Add Ansible binary modules + * cattlectl_apply + * cattlectl_list + * cattlectl_delete * (#48) Add support for multiple YAML objects in a single file * Each not empty object must have fields * `api_version` diff --git a/README.md b/README.md index 798431f..99e282f 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,20 @@ bitgrip/cattlectl apply Build from source ----------------- +### cattlectl + ```bash go install \ -ldflags "-X github.com/bitgrip/cattlectl/internal/pkg/ctl.Version=$(git describe --tags) -s -w" \ -a -tags netgo -installsuffix netgo -mod=vendor ``` +### Ansible modules + +``` +go build -mod=vendor -o ~/.ansible/plugins/modules/ ./ansible/... +``` + Docs ---- diff --git a/ansible/cattlectl_apply/main.go b/ansible/cattlectl_apply/main.go new file mode 100644 index 0000000..4bc28a4 --- /dev/null +++ b/ansible/cattlectl_apply/main.go @@ -0,0 +1,92 @@ +// Copyright © 2020 Bitgrip +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/bitgrip/cattlectl/ansible/utils" + "github.com/bitgrip/cattlectl/internal/pkg/ctl" + "github.com/bitgrip/cattlectl/internal/pkg/rancher/descriptor" + "github.com/bitgrip/cattlectl/internal/pkg/template" +) + +type moduleArgs struct { + ApplyFile string `json:"file"` + ValueFiles []string `json:"value_files"` + Values map[string]interface{} `json:"values"` + WorkingDirectory string `json:"working_directory"` + utils.AccessArgs `json:",inline"` +} + +type listResponse struct { + ApplyResult descriptor.ConvergeResult `json:"apply_result"` + utils.BaseResponse `json:",inline"` +} + +func main() { + var moduleArgs moduleArgs + utils.ReadArguments(&moduleArgs) + + var response listResponse + response.Version = ctl.Version + + if moduleArgs.WorkingDirectory != "" { + err := os.Chdir(moduleArgs.WorkingDirectory) + if err != nil { + response.Msg = fmt.Sprintf("Failed to apply file %s: - %v", moduleArgs.ApplyFile, err) + response.Failed = true + utils.FailJson(response) + } + } + + values, err := utils.LoadValues(moduleArgs.Values, moduleArgs.ValueFiles...) + if err != nil { + response.Msg = fmt.Sprintf("Failed to apply file %s: - %v", moduleArgs.ApplyFile, err) + response.Failed = true + utils.FailJson(response) + } + fileContent, err := ioutil.ReadFile(moduleArgs.ApplyFile) + if err != nil { + response.Msg = fmt.Sprintf("Failed to apply file %s: - %v", moduleArgs.ApplyFile, err) + response.Failed = true + utils.FailJson(response) + } + projectData, err := template.BuildTemplate(fileContent, values, filepath.Dir(moduleArgs.ApplyFile), false) + if err != nil { + response.Msg = fmt.Sprintf("Failed to apply file %s: - %v", moduleArgs.ApplyFile, err) + response.Failed = true + utils.FailJson(response) + } + + result, err := ctl.ApplyDescriptor( + moduleArgs.ApplyFile, + projectData, + map[string]interface{}{}, + utils.BuildRancherConfig(moduleArgs.AccessArgs), + ) + + if err != nil { + response.Msg = "Failed to apply descriptor: " + err.Error() + response.Failed = true + utils.FailJson(response) + } + response.ApplyResult = result + response.Changed = len(result.CreatedResources) > 0 || len(result.UpgradedResources) > 0 + utils.ExitJson(response) +} diff --git a/ansible/cattlectl_delete/main.go b/ansible/cattlectl_delete/main.go new file mode 100644 index 0000000..af3f776 --- /dev/null +++ b/ansible/cattlectl_delete/main.go @@ -0,0 +1,63 @@ +// Copyright © 2020 Bitgrip +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + + "github.com/bitgrip/cattlectl/ansible/utils" + "github.com/bitgrip/cattlectl/internal/pkg/ctl" +) + +type moduleArgs struct { + ProjectName string `json:"project_name"` + Namespace string `json:"namespace"` + Kind string `json:"kind"` + Names []string `json:"names"` + utils.AccessArgs `json:",inline"` +} + +type listResponse struct { + Deleted []string `json:"deleted"` + utils.BaseResponse `json:",inline"` +} + +func main() { + var moduleArgs moduleArgs + utils.ReadArguments(&moduleArgs) + + var response listResponse + response.Version = ctl.Version + for _, name := range moduleArgs.Names { + deleted, err := ctl.DeleteProjectResouce( + moduleArgs.ProjectName, + moduleArgs.Namespace, + moduleArgs.Kind, + name, + utils.BuildRancherConfig(moduleArgs.AccessArgs), + ) + if err != nil { + response.Msg = fmt.Sprintf("Failed to delete %s %s: - %v", moduleArgs.Kind, name, err) + response.Failed = true + utils.FailJson(response) + } + if deleted { + response.Deleted = append(response.Deleted, name) + response.Changed = true + } + } + response.Msg = fmt.Sprintf("Deleted %v %ss", len(response.Deleted), moduleArgs.Kind) + utils.ExitJson(response) +} diff --git a/ansible/cattlectl_list/main.go b/ansible/cattlectl_list/main.go new file mode 100644 index 0000000..1a494d8 --- /dev/null +++ b/ansible/cattlectl_list/main.go @@ -0,0 +1,59 @@ +// Copyright © 2020 Bitgrip +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + + "github.com/bitgrip/cattlectl/ansible/utils" + "github.com/bitgrip/cattlectl/internal/pkg/ctl" +) + +type moduleArgs struct { + ProjectName string `json:"project_name"` + Namespace string `json:"namespace"` + Kind string `json:"kind"` + Pattern string `json:"pattern"` + utils.AccessArgs `json:",inline"` +} + +type listResponse struct { + Matches []string `json:"matches"` + utils.BaseResponse `json:",inline"` +} + +func main() { + var moduleArgs moduleArgs + utils.ReadArguments(&moduleArgs) + + matches, err := ctl.ListProjectResouces( + moduleArgs.ProjectName, + moduleArgs.Namespace, + moduleArgs.Kind, + moduleArgs.Pattern, + utils.BuildRancherConfig(moduleArgs.AccessArgs), + ) + + var response listResponse + response.Version = ctl.Version + if err != nil { + response.Msg = "Failed to list resources: " + err.Error() + response.Failed = true + utils.FailJson(response) + } + response.Msg = fmt.Sprintf("List %v matches", len(matches)) + response.Matches = matches + utils.ExitJson(response) +} diff --git a/ansible/utils/util.go b/ansible/utils/util.go new file mode 100644 index 0000000..2d06767 --- /dev/null +++ b/ansible/utils/util.go @@ -0,0 +1,142 @@ +// Copyright © 2020 Bitgrip +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "github.com/bitgrip/cattlectl/internal/pkg/config" + "github.com/bitgrip/cattlectl/internal/pkg/ctl" + "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" +) + +type AccessArgs struct { + RancherURL string `json:"rancher_url"` + InsecureAPI bool `json:"insecure_api"` + CACerts string `json:"ca_certs"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + ClusterName string `json:"cluster_name"` + ConfigFile string `json:"config_file"` + DryRun bool `json:"dry_run"` +} + +type BaseResponse struct { + Msg string `json:"msg"` + Changed bool `json:"changed"` + Failed bool `json:"failed"` + Version string `json:"cattlectl_version"` +} + +func ExitJson(responseBody interface{}) { + ReturnResponse(responseBody, false) +} + +func FailJson(responseBody interface{}) { + ReturnResponse(responseBody, true) +} + +func ReadArguments(moduleArgs interface{}) { + + var response BaseResponse + response.Version = ctl.Version + + if len(os.Args) != 2 { + response.Msg = "No argument file provided" + response.Failed = true + FailJson(response) + } + + argsFile := os.Args[1] + + text, err := ioutil.ReadFile(argsFile) + if err != nil { + response.Msg = "Could not read configuration file: " + argsFile + response.Failed = true + FailJson(response) + } + + err = json.Unmarshal(text, moduleArgs) + if err != nil { + response.Msg = "Configuration file not valid JSON: " + argsFile + response.Failed = true + FailJson(response) + } + + os.Chdir("") +} + +func ReturnResponse(responseBody interface{}, failed bool) { + var response []byte + var err error + response, err = json.Marshal(responseBody) + if err != nil { + response, _ = json.Marshal(BaseResponse{Msg: "Invalid response object", Failed: true}) + failed = true + } + fmt.Println(string(response)) + if failed { + os.Exit(1) + } else { + os.Exit(0) + } +} + +func BuildRancherConfig(args AccessArgs) config.Config { + if args.ConfigFile != "" { + viper.SetConfigFile(args.ConfigFile) + } else { + home, err := homedir.Dir() + if err != nil { + FailJson(BaseResponse{Msg: "Can not read configuration: " + err.Error()}) + } + viper.AddConfigPath(home) + viper.SetConfigName(".cattlectl") + } + viper.ReadInConfig() + if args.RancherURL == "" { + args.RancherURL = viper.GetString("rancher.url") + } + if args.InsecureAPI == false { + args.InsecureAPI = viper.GetBool("rancher.insecure_api") + } + if args.CACerts == "" { + args.CACerts = viper.GetString("rancher.ca_certs") + } + if args.AccessKey == "" { + args.AccessKey = viper.GetString("rancher.access_key") + } + if args.SecretKey == "" { + args.SecretKey = viper.GetString("rancher.secret_key") + } + if args.ClusterName == "" { + args.ClusterName = viper.GetString("rancher.cluster_name") + } + return config.SimpleConfig( + args.RancherURL, + args.InsecureAPI, + args.CACerts, + args.AccessKey, + args.SecretKey, + args.ClusterName, + "", + false, + args.DryRun, + ) +} diff --git a/ansible/utils/values.go b/ansible/utils/values.go new file mode 100644 index 0000000..03f692b --- /dev/null +++ b/ansible/utils/values.go @@ -0,0 +1,95 @@ +// Copyright © 2018 Bitgrip +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/sirupsen/logrus" + "github.com/spf13/afero" + "github.com/spf13/viper" + yaml "gopkg.in/yaml.v2" +) + +var osFs = afero.NewOsFs() + +// LoadValues is reading values from a optional values file (YAML formated) +// +// The values are merged with corresponding environment variables. +func LoadValues(values map[string]interface{}, valuesFiles ...string) (map[string]interface{}, error) { + valuesConfig := viper.New() + valuesConfig.SetConfigType("yaml") + for _, valuesFile := range valuesFiles { + var absValuesFile string + var file []byte + var err error + if absValuesFile, err = filepath.Abs(valuesFile); err != nil { + return nil, err + } + logger := logrus.WithField("values-file", absValuesFile) + if fileExists, _ := afero.Exists(osFs, absValuesFile); !fileExists { + logger.Debug("values dose not exists") + continue + } + if err := verifyValuesFile(absValuesFile); err != nil { + return nil, err + } + if file, err = afero.ReadFile(osFs, absValuesFile); err != nil { + return nil, err + } + logger.Debug("load values") + valuesConfig.MergeConfig(bytes.NewReader(file)) + } + valuesConfig.MergeConfigMap(values) + valuesConfig.AutomaticEnv() + valuesConfig.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + for _, name := range viper.GetStringSlice("env_value_keys") { + valuesConfig.BindEnv(name) + } + return valuesConfig.AllSettings(), nil +} + +func verifyValuesFile(valuesFile string) error { + expected := make(map[string]interface{}, 0) + fileContent, err := ioutil.ReadFile(valuesFile) + if err != nil && os.IsNotExist(err) { + // A not existing values file is valid + return nil + } else if err != nil { + return err + } + err = yaml.Unmarshal(fileContent, &expected) + if err != nil { + return err + } + valuesConfig := viper.New() + valuesConfig.SetConfigFile(valuesFile) + valuesConfig.ReadInConfig() + structureFromViper, _ := yaml.Marshal(valuesConfig.AllSettings()) + actual := make(map[string]interface{}, 0) + yaml.Unmarshal(structureFromViper, &actual) + + if !reflect.DeepEqual(expected, actual) { + return fmt.Errorf("uppercase characters are not allowed on value keys") + + } + return nil +} diff --git a/cmd/apply/apply.go b/cmd/apply/apply.go index 3bb6eb5..e73d578 100644 --- a/cmd/apply/apply.go +++ b/cmd/apply/apply.go @@ -43,7 +43,6 @@ var ( // used services var ( - doApply = ctl.ApplyProject doApplyDescriptor = ctl.ApplyDescriptor newProjectParser = project.NewProjectParser ) @@ -74,12 +73,16 @@ func apply(cmd *cobra.Command, args []string) { Fatal(err) } - err = doApplyDescriptor(applyFile, projectData, values, rootConfig) + result, err := doApplyDescriptor(applyFile, projectData, values, rootConfig) if err != nil { logrus.WithFields(values). WithField("apply_file", applyFile). Fatal(err) } + logrus. + WithField("upgraded-resouces", len(result.UpgradedResources)). + WithField("created-resouces", len(result.CreatedResources)). + Info("Finished Apply") } func init() { diff --git a/cmd/config.go b/cmd/config.go index 3caf13d..4af7803 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -60,3 +60,7 @@ func (config) ClusterID() string { func (config) MergeAnswers() bool { return viper.GetBool("rancher.merge_answers") } + +func (config) DryRun() bool { + return viper.GetBool("dry_run") +} diff --git a/cmd/delete/delete.go b/cmd/delete/delete.go index f2553f9..862ed74 100644 --- a/cmd/delete/delete.go +++ b/cmd/delete/delete.go @@ -25,7 +25,7 @@ import ( var ( validArgs = []string{"app"} deleteCmd = &cobra.Command{ - Use: "delete TYPE NAME", + Use: "delete KIND NAME", Short: "Deletes an rancher resouce", Long: deleteLongDescription, Run: delete, @@ -43,28 +43,29 @@ func BaseCommand(config config.Config, init func()) *cobra.Command { } func delete(cmd *cobra.Command, args []string) { - if len(args) != 2 { + if len(args) < 2 { logrus.Warn(cmd.UsageString()) return } - resouceType := args[0] - resourceName := args[1] + kind := args[0] projectName := viper.GetString("delete_cmd.project_name") namespace := viper.GetString("delete_cmd.namespace") - logrus. - WithField("project-name", projectName). - WithField("resouce-type", resouceType). - WithField("resouce-name", resourceName). - WithField("cluster-name", rootConfig.ClusterName()). - Info("Delete project resouce") - err := ctl.DeleteProjectResouce(projectName, namespace, resouceType, resourceName, rootConfig) - if err != nil { + for _, resourceName := range args[1:] { logrus. WithField("project-name", projectName). - WithField("resouce-type", resouceType). + WithField("kind", kind). WithField("resouce-name", resourceName). WithField("cluster-name", rootConfig.ClusterName()). - Fatal(err) + Info("Delete project resouce") + _, err := ctl.DeleteProjectResouce(projectName, namespace, kind, resourceName, rootConfig) + if err != nil { + logrus. + WithField("project-name", projectName). + WithField("kind", kind). + WithField("resouce-name", resourceName). + WithField("cluster-name", rootConfig.ClusterName()). + Fatal(err) + } } } diff --git a/cmd/list/lsit.go b/cmd/list/list.go similarity index 90% rename from cmd/list/lsit.go rename to cmd/list/list.go index b1d5501..178a7b5 100644 --- a/cmd/list/lsit.go +++ b/cmd/list/list.go @@ -15,6 +15,8 @@ package list import ( + "fmt" + "github.com/bitgrip/cattlectl/internal/pkg/config" "github.com/bitgrip/cattlectl/internal/pkg/ctl" "github.com/sirupsen/logrus" @@ -25,7 +27,7 @@ import ( var ( validArgs = []string{"app"} listCmd = &cobra.Command{ - Use: "list TYPE", + Use: "list KIND", Short: "Lists an rancher resouce", Long: "Lists an rancher resouce", Run: list, @@ -47,23 +49,26 @@ func list(cmd *cobra.Command, args []string) { logrus.Warn(cmd.UsageString()) return } - resouceType := args[0] + kind := args[0] projectName := viper.GetString("list_cmd.project_name") namespace := viper.GetString("list_cmd.namespace") pattern := viper.GetString("list_cmd.pattern") logrus. WithField("project-name", projectName). - WithField("resouce-type", resouceType). + WithField("kind", kind). WithField("cluster-name", rootConfig.ClusterName()). Debug("List project resouces") - err := ctl.ListProjectResouces(projectName, namespace, resouceType, pattern, rootConfig) + matches, err := ctl.ListProjectResouces(projectName, namespace, kind, pattern, rootConfig) if err != nil { logrus. WithField("project-name", projectName). - WithField("resouce-type", resouceType). + WithField("kind", kind). WithField("cluster-name", rootConfig.ClusterName()). Fatal(err) } + for _, match := range matches { + fmt.Println(match) + } } func init() { diff --git a/cmd/root.go b/cmd/root.go index 92f96cf..b6a288a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -62,6 +62,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cattlectl.yaml)") rootCmd.PersistentFlags().IntVarP(&LogLevel, "verbosity", "v", 0, "verbosity level to use") rootCmd.PersistentFlags().BoolVar(&logJson, "log-json", false, "if to log using json format") + rootCmd.PersistentFlags().Bool("dry-run", false, "if do dry-run") rootCmd.PersistentFlags().String("rancher-url", "", "The URL to reach the rancher") rootCmd.PersistentFlags().Bool("insecure-api", false, "If Rancher uses a self signed certificate") rootCmd.PersistentFlags().String("access-key", "", "The access key to access rancher with") @@ -88,6 +89,9 @@ func init() { viper.BindPFlag("rancher.cluster_name", rootCmd.PersistentFlags().Lookup("cluster-name")) viper.BindEnv("rancher.cluster_name", "RANCHER_CLUSTER_NAME") + viper.BindPFlag("dry_run", rootCmd.PersistentFlags().Lookup("dry-run")) + viper.BindEnv("dry_run", "DRY_RUN") + rootCmd.AddCommand(apply.BaseCommand(rancherConfig, initSubCommand)) rootCmd.AddCommand(delete.BaseCommand(rancherConfig, initSubCommand)) rootCmd.AddCommand(list.BaseCommand(rancherConfig, initSubCommand)) diff --git a/docs/ansible/cattlectl_apply.md b/docs/ansible/cattlectl_apply.md new file mode 100644 index 0000000..3b5ceee --- /dev/null +++ b/docs/ansible/cattlectl_apply.md @@ -0,0 +1,48 @@ +cattleclt_apply +=============== + +Synopsis +-------- + +* Applies a descriptor to a Rancher server +* Uses a set of value files and a dict with highest precedence to execute the descriptor template + +Parameters +---------- + +### Specific parameters + +| Parameter | Choices/Defaults | Comments | +|---|---|---| +|file
string| __Default:__
"" | The file containing the descriptor template to apply| +|value_files
list| __Default:__
[] | The set of value files to use when executing the descriptor template| +|values
dict| __Default:__
{} | Dict of values with highest precedence when executing the descriptor template| +|working_directory
string| __Default:__
"" |If set all relative files are relative to `working_directory`
Relative to the playbook directory otherwais | + +### General parameters + +| Parameter | Choices/Defaults | Comments | +|---|---|---| +| rancher_url
string | __Default:__
"" | The URL to access the Rancher server
Read from `config_file` if absent | +| insecure_api
boolean | __Choices:__
no ←
yes | If cattlectl is going to accept insecure api
Read from `config_file` if absent | +| ca_certs
string | __Default:__
"" | Certs of a private CA if requierd
Read from `config_file` if absent | +| access_key
string | __Default:__
"" | Access key to gain access to the Rancher server
Read from `config_file` if absent | +| secret_key
string | __Default:__
"" | Secret key to authenticate the `access_key`
Read from `config_file` if absent | +| cluster_name
string | __Default:__
"" | The name of the Cluster to access via Rancher
Read from `config_file` if absent | +| config_file
string | __Default:__
~/.cattlectl.yaml| The location of the cattlectl config file to use | +| dry_run
boolean | __Choices:__
no ←
yes | If true all `write` operations are logged only | + +Examples +-------- + +```yaml + - name: apply project descriptor + cattlectl_apply: + file: "descriptors/project.yaml" + value_files: + - values/general.yaml + - values/{{cluster_name}}/values.yaml + - values/{{ cluster_name }}/{{ project_name }}.yaml + values: + my_extra_value: "extra-value" +``` diff --git a/docs/ansible/cattlectl_delete.md b/docs/ansible/cattlectl_delete.md new file mode 100644 index 0000000..7507042 --- /dev/null +++ b/docs/ansible/cattlectl_delete.md @@ -0,0 +1,45 @@ +cattleclt_delete +================ + +Synopsis +-------- + +* Deletes on or more resources from a Rancher server + +Parameters +---------- + +### Specific parameters + +| Parameter | Choices/Defaults | Comments | +|---|---|---| +| project_name
string | __Default:__
"" | The project to delete resources from
ignored for kind project | +| namespace
string | __Default:__
"" | The namespace to delete resources from
ignored for kind namespace and project | +| kind
string | __Default:__
"" | The kind of resource to delete | +| names
list | __Default:__
[] | The names of the resources to delete | + +### General parameters + +| Parameter | Choices/Defaults | Comments | +|---|---|---| +| rancher_url
string | __Default:__
"" | The URL to access the Rancher server
Read from `config_file` if absent | +| insecure_api
boolean | __Choices:__
no ←
yes | If cattlectl is going to accept insecure api
Read from `config_file` if absent | +| ca_certs
string | __Default:__
"" | Certs of a private CA if requierd
Read from `config_file` if absent | +| access_key
string | __Default:__
"" | Access key to gain access to the Rancher server
Read from `config_file` if absent | +| secret_key
string | __Default:__
"" | Secret key to authenticate the `access_key`
Read from `config_file` if absent | +| cluster_name
string | __Default:__
"" | The name of the Cluster to access via Rancher
Read from `config_file` if absent | +| config_file
string | __Default:__
~/.cattlectl.yaml| The location of the cattlectl config file to use | +| dry_run
boolean | __Choices:__
no ←
yes | If true all `write` operations are logged only | + +Examples +-------- + +```yaml +- name: delete namespaces + cattlectl_delete: + project_name: my-project + kind: namespace + names: + - my-namespace01 + - my-namespace02 +``` diff --git a/docs/ansible/cattlectl_list.md b/docs/ansible/cattlectl_list.md new file mode 100644 index 0000000..6efa3c6 --- /dev/null +++ b/docs/ansible/cattlectl_list.md @@ -0,0 +1,45 @@ +cattleclt_list +=============== + +Synopsis +-------- + +* List resources from a Rancher server +* Matches by an optional name pattern + +Parameters +---------- + +### Specific parameters + +| Parameter | Choices/Defaults | Comments | +|---|---|---| +| project_name
string | __Default:__
"" | The project to list resources from
ignored for kind project | +| namespace
string | __Default:__
"" | The namespace to list resources from
ignored for kind namespace and project | +| kind
string | __Default:__
"" | The kind of resource to list | +| pattern
string | __Default:__
"" | A pattern to match listed names with | + + +### General parameters + +| Parameter | Choices/Defaults | Comments | +|---|---|---| +| rancher_url
string | __Default:__
"" | The URL to access the Rancher server
Read from `config_file` if absent | +| insecure_api
boolean | __Choices:__
no ←
yes | If cattlectl is going to accept insecure api
Read from `config_file` if absent | +| ca_certs
string | __Default:__
"" | Certs of a private CA if requierd
Read from `config_file` if absent | +| access_key
string | __Default:__
"" | Access key to gain access to the Rancher server
Read from `config_file` if absent | +| secret_key
string | __Default:__
"" | Secret key to authenticate the `access_key`
Read from `config_file` if absent | +| cluster_name
string | __Default:__
"" | The name of the Cluster to access via Rancher
Read from `config_file` if absent | +| config_file
string | __Default:__
~/.cattlectl.yaml| The location of the cattlectl config file to use | +| dry_run
boolean | __Choices:__
no ←
yes | If true all `write` operations are logged only | + +Examples +-------- + +```yaml +- name: list namespaces + cattlectl_list: + project_name: my-project + kind: namespace + pattern: my- +``` diff --git a/docs/ansible/index.md b/docs/ansible/index.md new file mode 100644 index 0000000..fa1f39e --- /dev/null +++ b/docs/ansible/index.md @@ -0,0 +1,20 @@ +cattlectl - ansible modules +=========================== + +cattlectl_apply +--------------- + +* Apply a descriptor to a Rancher server +* [module docs](cattlectl_apply.md) + +cattlectl_list +-------------- + +* List the names of a kind of Rancher Objects +* [module docs](cattlectl_list.md) + +cattlectl_delete +---------------- + +* Delete a list of Rancher Objects +* [module docs](cattlectl_delete.md) diff --git a/docs/cattlectl.md b/docs/cli/cattlectl.md similarity index 97% rename from docs/cattlectl.md rename to docs/cli/cattlectl.md index f888071..f912dfc 100644 --- a/docs/cattlectl.md +++ b/docs/cli/cattlectl.md @@ -19,6 +19,7 @@ deployement, if you run cattlectl twice. --cluster-id string The ID of the cluster the project is part of --cluster-name string The name of the cluster the project is part of --config string config file (default is $HOME/.cattlectl.yaml) + --dry-run if do dry-run -h, --help help for cattlectl --insecure-api If Rancher uses a self signed certificate --log-json if to log using json format diff --git a/docs/cattlectl_apply.md b/docs/cli/cattlectl_apply.md similarity index 97% rename from docs/cattlectl_apply.md rename to docs/cli/cattlectl_apply.md index 2f556a7..60d110c 100644 --- a/docs/cattlectl_apply.md +++ b/docs/cli/cattlectl_apply.md @@ -51,6 +51,7 @@ cattlectl apply [flags] --cluster-id string The ID of the cluster the project is part of --cluster-name string The name of the cluster the project is part of --config string config file (default is $HOME/.cattlectl.yaml) + --dry-run if do dry-run --insecure-api If Rancher uses a self signed certificate --log-json if to log using json format --rancher-url string The URL to reach the rancher diff --git a/docs/cattlectl_completion.md b/docs/cli/cattlectl_completion.md similarity index 96% rename from docs/cattlectl_completion.md rename to docs/cli/cattlectl_completion.md index fae1cf1..4894c12 100644 --- a/docs/cattlectl_completion.md +++ b/docs/cli/cattlectl_completion.md @@ -40,6 +40,7 @@ cattlectl completion [flags] --cluster-id string The ID of the cluster the project is part of --cluster-name string The name of the cluster the project is part of --config string config file (default is $HOME/.cattlectl.yaml) + --dry-run if do dry-run --insecure-api If Rancher uses a self signed certificate --log-json if to log using json format --rancher-url string The URL to reach the rancher diff --git a/docs/cattlectl_delete.md b/docs/cli/cattlectl_delete.md similarity index 94% rename from docs/cattlectl_delete.md rename to docs/cli/cattlectl_delete.md index 7258c74..4fe2b36 100644 --- a/docs/cattlectl_delete.md +++ b/docs/cli/cattlectl_delete.md @@ -21,7 +21,7 @@ Deletes an rancher resouce. * stateful-set - NOT YET IMPLEMENTED ``` -cattlectl delete TYPE NAME [flags] +cattlectl delete KIND NAME [flags] ``` ### Options @@ -39,6 +39,7 @@ cattlectl delete TYPE NAME [flags] --cluster-id string The ID of the cluster the project is part of --cluster-name string The name of the cluster the project is part of --config string config file (default is $HOME/.cattlectl.yaml) + --dry-run if do dry-run --insecure-api If Rancher uses a self signed certificate --log-json if to log using json format --rancher-url string The URL to reach the rancher diff --git a/docs/cattlectl_gen-doc.md b/docs/cli/cattlectl_gen-doc.md similarity index 95% rename from docs/cattlectl_gen-doc.md rename to docs/cli/cattlectl_gen-doc.md index 2a7d826..7b71046 100644 --- a/docs/cattlectl_gen-doc.md +++ b/docs/cli/cattlectl_gen-doc.md @@ -23,6 +23,7 @@ cattlectl gen-doc [target folder] [flags] --cluster-id string The ID of the cluster the project is part of --cluster-name string The name of the cluster the project is part of --config string config file (default is $HOME/.cattlectl.yaml) + --dry-run if do dry-run --insecure-api If Rancher uses a self signed certificate --log-json if to log using json format --rancher-url string The URL to reach the rancher diff --git a/docs/cattlectl_list.md b/docs/cli/cattlectl_list.md similarity index 93% rename from docs/cattlectl_list.md rename to docs/cli/cattlectl_list.md index 9a87531..e0b242c 100644 --- a/docs/cattlectl_list.md +++ b/docs/cli/cattlectl_list.md @@ -7,7 +7,7 @@ Lists an rancher resouce Lists an rancher resouce ``` -cattlectl list TYPE [flags] +cattlectl list KIND [flags] ``` ### Options @@ -26,6 +26,7 @@ cattlectl list TYPE [flags] --cluster-id string The ID of the cluster the project is part of --cluster-name string The name of the cluster the project is part of --config string config file (default is $HOME/.cattlectl.yaml) + --dry-run if do dry-run --insecure-api If Rancher uses a self signed certificate --log-json if to log using json format --rancher-url string The URL to reach the rancher diff --git a/docs/cattlectl_show.md b/docs/cli/cattlectl_show.md similarity index 96% rename from docs/cattlectl_show.md rename to docs/cli/cattlectl_show.md index 8f1774f..798a5c1 100644 --- a/docs/cattlectl_show.md +++ b/docs/cli/cattlectl_show.md @@ -27,6 +27,7 @@ cattlectl show [flags] --cluster-id string The ID of the cluster the project is part of --cluster-name string The name of the cluster the project is part of --config string config file (default is $HOME/.cattlectl.yaml) + --dry-run if do dry-run --insecure-api If Rancher uses a self signed certificate --log-json if to log using json format --rancher-url string The URL to reach the rancher diff --git a/docs/cattlectl_version.md b/docs/cli/cattlectl_version.md similarity index 95% rename from docs/cattlectl_version.md rename to docs/cli/cattlectl_version.md index 4a73deb..6a4e550 100644 --- a/docs/cattlectl_version.md +++ b/docs/cli/cattlectl_version.md @@ -23,6 +23,7 @@ cattlectl version [flags] --cluster-id string The ID of the cluster the project is part of --cluster-name string The name of the cluster the project is part of --config string config file (default is $HOME/.cattlectl.yaml) + --dry-run if do dry-run --insecure-api If Rancher uses a self signed certificate --log-json if to log using json format --rancher-url string The URL to reach the rancher diff --git a/docs/index.md b/docs/index.md index c930303..4eeb3b7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -85,4 +85,4 @@ Finding more informations * [RancherDescriptor data model](rancher_descriptor.md) * [ClusterDescriptor data model](cluster_descriptor.md) * [ProjectDescriptor data model](project_descriptor.md) -* [cattlectl CLI documentation](cattlectl.md) +* [cattlectl CLI documentation](cli/cattlectl.md) diff --git a/go.mod b/go.mod index 90aa86a..245b25c 100644 --- a/go.mod +++ b/go.mod @@ -29,19 +29,15 @@ replace ( require ( github.com/Masterminds/semver v1.4.2 - github.com/google/gofuzz v1.0.0 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect - github.com/magiconair/properties v1.8.1 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/pelletier/go-toml v1.4.0 // indirect - github.com/pkg/errors v0.8.1 // indirect github.com/rancher/norman v0.0.0-20200227003532-35fa47cccad7 github.com/rancher/types v0.0.0-20191226170233-4d49bbf42146 github.com/sergi/go-diff v1.0.0 github.com/sirupsen/logrus v1.4.2 github.com/spf13/afero v1.2.2 github.com/spf13/cobra v0.0.5 - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.4.0 gopkg.in/yaml.v2 v2.2.8 k8s.io/apimachinery v0.0.0 diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index a766a2d..0a5338a 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -1,4 +1,4 @@ -// Copyright © 2018 Bitgrip +// Copyright © 2020 Bitgrip // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,6 +14,8 @@ package config +import "fmt" + // Config provides rancher access informations type Config interface { RancherURL() string @@ -25,4 +27,81 @@ type Config interface { ClusterName() string ClusterID() string MergeAnswers() bool + DryRun() bool +} + +func SimpleConfig( + rancherURL string, + insecureAPI bool, + caCerts string, + accessKey string, + secretKey string, + clusterName string, + clusterID string, + mergeAnswers bool, + dryRun bool, +) Config { + return simpleConfig{ + rancherURL: rancherURL, + insecureAPI: insecureAPI, + caCerts: caCerts, + accessKey: accessKey, + secretKey: secretKey, + clusterName: clusterName, + clusterID: clusterID, + mergeAnswers: mergeAnswers, + dryRun: dryRun, + } +} + +type simpleConfig struct { + rancherURL string + insecureAPI bool + caCerts string + accessKey string + secretKey string + clusterName string + clusterID string + mergeAnswers bool + dryRun bool +} + +func (config simpleConfig) RancherURL() string { + return config.rancherURL +} + +func (config simpleConfig) InsecureAPI() bool { + return config.insecureAPI +} + +func (config simpleConfig) CACerts() string { + return config.caCerts +} + +func (config simpleConfig) AccessKey() string { + return config.accessKey +} + +func (config simpleConfig) SecretKey() string { + return config.secretKey +} + +func (config simpleConfig) TokenKey() string { + return fmt.Sprintf("%s:%s", config.AccessKey(), config.SecretKey()) +} + +func (config simpleConfig) ClusterName() string { + return config.clusterName +} + +func (config simpleConfig) ClusterID() string { + return config.clusterID +} + +func (config simpleConfig) MergeAnswers() bool { + return config.mergeAnswers +} + +func (config simpleConfig) DryRun() bool { + return config.dryRun } diff --git a/internal/pkg/ctl/apply.go b/internal/pkg/ctl/apply.go index 8b473fe..9ca87d8 100644 --- a/internal/pkg/ctl/apply.go +++ b/internal/pkg/ctl/apply.go @@ -26,6 +26,7 @@ import ( clusterModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/model" "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + "github.com/bitgrip/cattlectl/internal/pkg/rancher/descriptor" rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" yaml "gopkg.in/yaml.v2" ) @@ -55,199 +56,205 @@ var ( ) // ApplyDescriptor the the CTL perform a apply action -func ApplyDescriptor(file string, fullData []byte, values map[string]interface{}, config config.Config) error { +func ApplyDescriptor(file string, fullData []byte, values map[string]interface{}, config config.Config) (result descriptor.ConvergeResult, err error) { decoder := yaml.NewDecoder(bytes.NewReader(fullData)) for { - apiVersion, kind, object, err := DecodeToApply(decoder) - if err != nil { - if err.Error() == "EMPTY" { + apiVersion, kind, object, decodeErr := DecodeToApply(decoder) + if decodeErr != nil { + if decodeErr.Error() == "EMPTY" { continue - } else if err.Error() == "EOF" { + } else if decodeErr.Error() == "EOF" { break } - return err + err = decodeErr + return } - singleObjectData, err := yaml.Marshal(object) - if err != nil { - return err + var ( + singleObjectData []byte + singleResult descriptor.ConvergeResult + ) + if singleObjectData, err = yaml.Marshal(object); err != nil { + return } if !isSupportedAPIVersion(apiVersion) { - return fmt.Errorf("Unsupported api version %s", apiVersion) + return result, fmt.Errorf("Unsupported api version %s", apiVersion) } switch kind { case rancherModel.RancherKind: descriptor := rancherModel.Rancher{} - if err := newRancherParser(file, values).Parse(singleObjectData, &descriptor); err != nil { - return err + if err = newRancherParser(file, values).Parse(singleObjectData, &descriptor); err != nil { + return } - if err := ApplyRancher(descriptor, config); err != nil { - return err + if singleResult, err = ApplyRancher(descriptor, config); err != nil { + return } case rancherModel.ClusterKind: descriptor := clusterModel.Cluster{} - if err := newClusterParser(file, values).Parse(singleObjectData, &descriptor); err != nil { - return err + if err = newClusterParser(file, values).Parse(singleObjectData, &descriptor); err != nil { + return } - if err := ApplyCluster(descriptor, config); err != nil { - return err + if singleResult, err = ApplyCluster(descriptor, config); err != nil { + return } case rancherModel.ProjectKind: project := projectModel.Project{} - if err := newProjectParser(file, values).Parse(singleObjectData, &project); err != nil { - return err + if err = newProjectParser(file, values).Parse(singleObjectData, &project); err != nil { + return } - if err := ApplyProject(project, config); err != nil { - return err + if singleResult, err = ApplyProject(project, config); err != nil { + return } case rancherModel.JobKind: jobDescriptor := projectModel.JobDescriptor{} - if err := newJobParser(file, values).Parse(singleObjectData, &jobDescriptor); err != nil { - return err + if err = newJobParser(file, values).Parse(singleObjectData, &jobDescriptor); err != nil { + return } - if err := ApplyJob(jobDescriptor, config); err != nil { - return err + if singleResult, err = ApplyJob(jobDescriptor, config); err != nil { + return } case rancherModel.CronJobKind: cronJobDescriptor := projectModel.CronJobDescriptor{} - if err := newCronJobParser(file, values).Parse(singleObjectData, &cronJobDescriptor); err != nil { - return err + if err = newCronJobParser(file, values).Parse(singleObjectData, &cronJobDescriptor); err != nil { + return } - if err := ApplyCronJob(cronJobDescriptor, config); err != nil { - return err + if singleResult, err = ApplyCronJob(cronJobDescriptor, config); err != nil { + return } case rancherModel.DeploymentKind: deploymentDescriptor := projectModel.DeploymentDescriptor{} - if err := newDeploymentParser(file, values).Parse(singleObjectData, &deploymentDescriptor); err != nil { - return err + if err = newDeploymentParser(file, values).Parse(singleObjectData, &deploymentDescriptor); err != nil { + return } - if err := ApplyDeployment(deploymentDescriptor, config); err != nil { - return err + if singleResult, err = ApplyDeployment(deploymentDescriptor, config); err != nil { + return } case rancherModel.DaemonSetKind: daemonSetDescriptor := projectModel.DaemonSetDescriptor{} - if err := newDaemonSetParser(file, values).Parse(singleObjectData, &daemonSetDescriptor); err != nil { - return err + if err = newDaemonSetParser(file, values).Parse(singleObjectData, &daemonSetDescriptor); err != nil { + return } - if err := ApplyDaemonSet(daemonSetDescriptor, config); err != nil { - return err + if singleResult, err = ApplyDaemonSet(daemonSetDescriptor, config); err != nil { + return } case rancherModel.StatefulSetKind: statefulSetDescriptor := projectModel.StatefulSetDescriptor{} - if err := newStatefulSetParser(file, values).Parse(singleObjectData, &statefulSetDescriptor); err != nil { - return err + if err = newStatefulSetParser(file, values).Parse(singleObjectData, &statefulSetDescriptor); err != nil { + return } - if err := ApplyStatefulSet(statefulSetDescriptor, config); err != nil { - return err + if singleResult, err = ApplyStatefulSet(statefulSetDescriptor, config); err != nil { + return } default: - return fmt.Errorf("Unknown descriptor %s", kind) + return result, fmt.Errorf("Unknown descriptor %s", kind) } + result.CreatedResources = append(result.CreatedResources, singleResult.CreatedResources...) + result.UpgradedResources = append(result.UpgradedResources, singleResult.UpgradedResources...) } - return nil + return } // ApplyCronJob the the CTL perform a apply action to a cronjob descriptor -func ApplyCronJob(cronJobDescriptor projectModel.CronJobDescriptor, config config.Config) error { +func ApplyCronJob(cronJobDescriptor projectModel.CronJobDescriptor, config config.Config) (result descriptor.ConvergeResult, err error) { _, _, projectClient, err := fillWorkloadMetadata(&cronJobDescriptor.Metadata, config) if err != nil { - return err + return } converger, err := newCronJobConverger(cronJobDescriptor, projectClient) if err != nil { - return err + return } - return converger.Converge() + return converger.Converge(config.DryRun()) } // ApplyJob the the CTL perform a apply action to a job descriptor -func ApplyJob(jobDescriptor projectModel.JobDescriptor, config config.Config) error { +func ApplyJob(jobDescriptor projectModel.JobDescriptor, config config.Config) (result descriptor.ConvergeResult, err error) { _, _, projectClient, err := fillWorkloadMetadata(&jobDescriptor.Metadata, config) if err != nil { - return err + return } converger, err := newJobConverger(jobDescriptor, projectClient) if err != nil { - return err + return } - return converger.Converge() + return converger.Converge(config.DryRun()) } // ApplyDeployment the the CTL perform a apply action to a deployment descriptor -func ApplyDeployment(deploymentDescriptor projectModel.DeploymentDescriptor, config config.Config) error { +func ApplyDeployment(deploymentDescriptor projectModel.DeploymentDescriptor, config config.Config) (result descriptor.ConvergeResult, err error) { _, _, projectClient, err := fillWorkloadMetadata(&deploymentDescriptor.Metadata, config) if err != nil { - return err + return } converger, err := newDeploymentConverger(deploymentDescriptor, projectClient) if err != nil { - return err + return } - return converger.Converge() + return converger.Converge(config.DryRun()) } // ApplyDaemonSet the the CTL perform a apply action to a daemon set descriptor -func ApplyDaemonSet(daemonSetDescriptor projectModel.DaemonSetDescriptor, config config.Config) error { +func ApplyDaemonSet(daemonSetDescriptor projectModel.DaemonSetDescriptor, config config.Config) (result descriptor.ConvergeResult, err error) { _, _, projectClient, err := fillWorkloadMetadata(&daemonSetDescriptor.Metadata, config) if err != nil { - return err + return } converger, err := newDaemonSetConverger(daemonSetDescriptor, projectClient) if err != nil { - return err + return } - return converger.Converge() + return converger.Converge(config.DryRun()) } // ApplyStatefulSet the the CTL perform a apply action to a stateful set descriptor -func ApplyStatefulSet(statefulSetDescriptor projectModel.StatefulSetDescriptor, config config.Config) error { +func ApplyStatefulSet(statefulSetDescriptor projectModel.StatefulSetDescriptor, config config.Config) (result descriptor.ConvergeResult, err error) { _, _, projectClient, err := fillWorkloadMetadata(&statefulSetDescriptor.Metadata, config) if err != nil { - return err + return } converger, err := newStatefulSetConverger(statefulSetDescriptor, projectClient) if err != nil { - return err + return } - return converger.Converge() + return converger.Converge(config.DryRun()) } // ApplyProject the the CTL perform a apply action to a project descriptor -func ApplyProject(project projectModel.Project, config config.Config) error { +func ApplyProject(project projectModel.Project, config config.Config) (result descriptor.ConvergeResult, err error) { _, clusterClient, err := fillProjectMetadata(&project.Metadata, config) if err != nil { - return err + return } converger, err := newProjectConverger(project, clusterClient) if err != nil { - return err + return } - return converger.Converge() + return converger.Converge(config.DryRun()) } // ApplyCluster the the CTL perform a apply action to a cluster descriptor -func ApplyCluster(cluster clusterModel.Cluster, config config.Config) error { +func ApplyCluster(cluster clusterModel.Cluster, config config.Config) (result descriptor.ConvergeResult, err error) { rancherClient, err := fillClusterMetadata(&cluster.Metadata, config) if err != nil { - return err + return } converger, err := newClusterConverger(cluster, rancherClient) if err != nil { - return err + return } - return converger.Converge() + return converger.Converge(config.DryRun()) } // ApplyRancher the the CTL perform a apply action to a cluster descriptor -func ApplyRancher(rancher rancherModel.Rancher, config config.Config) error { +func ApplyRancher(rancher rancherModel.Rancher, config config.Config) (result descriptor.ConvergeResult, err error) { rancherConfig, err := fillRancherMetadata(&rancher.Metadata, config) if err != nil { - return err + return } converger, err := newRancherConverger(rancher, rancherConfig) if err != nil { - return err + return } - return converger.Converge() + return converger.Converge(config.DryRun()) } // DecodeToApply will decode the next object from the decoder diff --git a/internal/pkg/ctl/apply_test.go b/internal/pkg/ctl/apply_test.go index 2c71a56..af426de 100644 --- a/internal/pkg/ctl/apply_test.go +++ b/internal/pkg/ctl/apply_test.go @@ -429,7 +429,7 @@ func TestApplyDescriptor(t *testing.T) { t.Run(tt.name, func(t *testing.T) { unexpectAllBackendCalls() tt.setExpectedBackends(t) - err := ApplyDescriptor(tt.args.file, tt.args.fullData, tt.args.values, tt.args.config) + _, err := ApplyDescriptor(tt.args.file, tt.args.fullData, tt.args.values, tt.args.config) if tt.wantErr != nil { assert.NotOk(t, err, tt.wantErr.Error()) } else { @@ -725,9 +725,9 @@ type testConverger struct { expected bool } -func (converger testConverger) Converge() (err error) { +func (converger testConverger) Converge(bool) (result descriptor.ConvergeResult, err error) { if !converger.expected { - return fmt.Errorf("Unexpected Call") + return result, fmt.Errorf("Unexpected Call") } return } @@ -759,6 +759,7 @@ type testConfig struct { clusterName string clusterID string mergeAnswers bool + dryRun bool } func (config testConfig) RancherURL() string { @@ -788,3 +789,6 @@ func (config testConfig) ClusterID() string { func (config testConfig) MergeAnswers() bool { return config.mergeAnswers } +func (config testConfig) DryRun() bool { + return config.dryRun +} diff --git a/internal/pkg/ctl/delete.go b/internal/pkg/ctl/delete.go index 2c28080..3d5af00 100644 --- a/internal/pkg/ctl/delete.go +++ b/internal/pkg/ctl/delete.go @@ -16,7 +16,7 @@ import ( ) var ( - deletableProjectResouceTypes = map[string]func(string, string, string, config.Config) error{ + deletableProjectResouceTypes = map[string]func(string, string, string, config.Config) (bool, error){ "namespace": deleteNamespace, "certificate": deleteCertificate, "config-map": deleteConfigMap, @@ -36,15 +36,15 @@ var ( // * projectName: the project to delete the resource from // * resourceType: the type of the resource to delete // * name: the name of the resource to delete -func DeleteProjectResouce(projectName, namespace, resouceType, name string, config config.Config) (err error) { - deleteFunc, supportedType := deletableProjectResouceTypes[resouceType] +func DeleteProjectResouce(projectName, namespace, kind, name string, config config.Config) (bool, error) { + deleteFunc, supportedType := deletableProjectResouceTypes[kind] if !supportedType { - return fmt.Errorf("Not supported resouce type [%s]", resouceType) + return false, fmt.Errorf("Not supported resouce type [%s]", kind) } return deleteFunc(projectName, namespace, name, config) } -func deleteNamespace(projectName, _namespace, name string, config config.Config) (err error) { +func deleteNamespace(projectName, _namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -55,10 +55,10 @@ func deleteNamespace(projectName, _namespace, name string, config config.Config) return } - return deleteProjectResouce(namespace, config.ClusterName(), projectName, "namespace", name) + return deleteProjectResouce(namespace, config.ClusterName(), projectName, "namespace", name, config.DryRun()) } -func deleteCertificate(projectName, namespace, name string, config config.Config) (err error) { +func deleteCertificate(projectName, namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -70,12 +70,12 @@ func deleteCertificate(projectName, namespace, name string, config config.Config } if namespace == "" { - return deleteProjectResouce(certificate, config.ClusterName(), projectName, "certificate", name) + return deleteProjectResouce(certificate, config.ClusterName(), projectName, "certificate", name, config.DryRun()) } - return deleteNamespaceResouce(certificate, config.ClusterName(), projectName, namespace, "certificate", name) + return deleteNamespaceResouce(certificate, config.ClusterName(), projectName, namespace, "certificate", name, config.DryRun()) } -func deleteConfigMap(projectName, namespace, name string, config config.Config) (err error) { +func deleteConfigMap(projectName, namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -86,10 +86,10 @@ func deleteConfigMap(projectName, namespace, name string, config config.Config) return } - return deleteNamespaceResouce(configMap, config.ClusterName(), projectName, namespace, "config-map", name) + return deleteNamespaceResouce(configMap, config.ClusterName(), projectName, namespace, "config-map", name, config.DryRun()) } -func deleteDockerCredential(projectName, namespace, name string, config config.Config) (err error) { +func deleteDockerCredential(projectName, namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -101,12 +101,12 @@ func deleteDockerCredential(projectName, namespace, name string, config config.C } if namespace == "" { - return deleteProjectResouce(dockerCredential, config.ClusterName(), projectName, "docker-credential", name) + return deleteProjectResouce(dockerCredential, config.ClusterName(), projectName, "docker-credential", name, config.DryRun()) } - return deleteNamespaceResouce(dockerCredential, config.ClusterName(), projectName, namespace, "docker-credential", name) + return deleteNamespaceResouce(dockerCredential, config.ClusterName(), projectName, namespace, "docker-credential", name, config.DryRun()) } -func deleteSecret(projectName, namespace, name string, config config.Config) (err error) { +func deleteSecret(projectName, namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -118,12 +118,12 @@ func deleteSecret(projectName, namespace, name string, config config.Config) (er } if namespace == "" { - return deleteProjectResouce(secret, config.ClusterName(), projectName, "secret", name) + return deleteProjectResouce(secret, config.ClusterName(), projectName, "secret", name, config.DryRun()) } - return deleteNamespaceResouce(secret, config.ClusterName(), projectName, namespace, "secret", name) + return deleteNamespaceResouce(secret, config.ClusterName(), projectName, namespace, "secret", name, config.DryRun()) } -func deleteApp(projectName, namespace, name string, config config.Config) (err error) { +func deleteApp(projectName, namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -134,10 +134,10 @@ func deleteApp(projectName, namespace, name string, config config.Config) (err e return } - return deleteProjectResouce(app, config.ClusterName(), projectName, "app", name) + return deleteProjectResouce(app, config.ClusterName(), projectName, "app", name, config.DryRun()) } -func deleteJob(projectName, namespace, name string, config config.Config) (err error) { +func deleteJob(projectName, namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -148,10 +148,10 @@ func deleteJob(projectName, namespace, name string, config config.Config) (err e return } - return deleteNamespaceResouce(job, config.ClusterName(), projectName, namespace, "job", name) + return deleteNamespaceResouce(job, config.ClusterName(), projectName, namespace, "job", name, config.DryRun()) } -func deleteCronJob(projectName, namespace, name string, config config.Config) (err error) { +func deleteCronJob(projectName, namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -162,10 +162,10 @@ func deleteCronJob(projectName, namespace, name string, config config.Config) (e return } - return deleteNamespaceResouce(cronJob, config.ClusterName(), projectName, namespace, "cron-job", name) + return deleteNamespaceResouce(cronJob, config.ClusterName(), projectName, namespace, "cron-job", name, config.DryRun()) } -func deleteDeployment(projectName, namespace, name string, config config.Config) (err error) { +func deleteDeployment(projectName, namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -176,10 +176,10 @@ func deleteDeployment(projectName, namespace, name string, config config.Config) return } - return deleteNamespaceResouce(deployment, config.ClusterName(), projectName, namespace, "deployment", name) + return deleteNamespaceResouce(deployment, config.ClusterName(), projectName, namespace, "deployment", name, config.DryRun()) } -func deleteDaemonSet(projectName, namespace, name string, config config.Config) (err error) { +func deleteDaemonSet(projectName, namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -190,10 +190,10 @@ func deleteDaemonSet(projectName, namespace, name string, config config.Config) return } - return deleteNamespaceResouce(daemonSet, config.ClusterName(), projectName, namespace, "daemon-set", name) + return deleteNamespaceResouce(daemonSet, config.ClusterName(), projectName, namespace, "daemon-set", name, config.DryRun()) } -func deleteStatefulSet(projectName, namespace, name string, config config.Config) (err error) { +func deleteStatefulSet(projectName, namespace, name string, config config.Config) (deleted bool, err error) { _, _, projectClient, err := getProjectClient(projectName, config) if err != nil { return @@ -204,40 +204,40 @@ func deleteStatefulSet(projectName, namespace, name string, config config.Config return } - return deleteNamespaceResouce(statefulSet, config.ClusterName(), projectName, namespace, "stateful-set", name) + return deleteNamespaceResouce(statefulSet, config.ClusterName(), projectName, namespace, "stateful-set", name, config.DryRun()) } -func deleteProjectResouce(resource client.ResourceClient, clusterName, projectName, resouceType, name string) (err error) { +func deleteProjectResouce(resource client.ResourceClient, clusterName, projectName, kind, name string, dryRun bool) (deleted bool, err error) { if exists, err := resource.Exists(); err != nil || !exists { if err != nil { - return err + return false, err } logrus. WithField("project-name", projectName). WithField("resouce-name", name). WithField("cluster-name", clusterName). - Infof("No %s skip delete", resouceType) - return nil + Infof("No %s skip delete", kind) + return false, nil } - err = resource.Delete() + deleted, err = resource.Delete(dryRun) return } -func deleteNamespaceResouce(resource client.ResourceClient, clusterName, projectName, namespace, resouceType, name string) (err error) { +func deleteNamespaceResouce(resource client.ResourceClient, clusterName, projectName, namespace, kind, name string, dryRun bool) (deleted bool, err error) { if exists, err := resource.Exists(); err != nil || !exists { if err != nil { - return err + return false, err } logrus. WithField("project-name", projectName). WithField("namespace", namespace). WithField("resouce-name", name). WithField("cluster-name", clusterName). - Infof("No %s skip delete", resouceType) - return nil + Infof("No %s skip delete", kind) + return false, nil } - err = resource.Delete() + deleted, err = resource.Delete(dryRun) return } diff --git a/internal/pkg/ctl/list.go b/internal/pkg/ctl/list.go index d122ed0..a5807a7 100644 --- a/internal/pkg/ctl/list.go +++ b/internal/pkg/ctl/list.go @@ -46,10 +46,11 @@ var ( // * projectName: the project to list the resources from // * namespace: the namespace to list the resources from // * resourceType: the type of the resources to list -func ListProjectResouces(projectName, namespace, resouceType, pattern string, config config.Config) (err error) { - listFunc, supportedType := listableProjectResouceTypes[resouceType] +// * pattern: a match pattern to filter the results +func ListProjectResouces(projectName, namespace, kind, pattern string, config config.Config) (matches []string, err error) { + listFunc, supportedType := listableProjectResouceTypes[kind] if !supportedType { - return fmt.Errorf("Not supported resouce type [%s]", resouceType) + return matches, fmt.Errorf("Not supported resouce type [%s]", kind) } names, err := listFunc(projectName, namespace, config) if err != nil { @@ -60,7 +61,7 @@ func ListProjectResouces(projectName, namespace, resouceType, pattern string, co if !matched { continue } - fmt.Println(name) + matches = append(matches, name) } return } diff --git a/internal/pkg/rancher/client/app_client.go b/internal/pkg/rancher/client/app_client.go index 47fb689..d026062 100644 --- a/internal/pkg/rancher/client/app_client.go +++ b/internal/pkg/rancher/client/app_client.go @@ -20,6 +20,7 @@ import ( "strings" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendProjectClient "github.com/rancher/types/client/project/v3" "github.com/sirupsen/logrus" @@ -72,6 +73,10 @@ type appClient struct { projectClient ProjectClient } +func (client *appClient) Type() string { + return rancherModel.App +} + func (client *appClient) Exists() (bool, error) { backendClient, err := client.projectClient.backendProjectClient() if err != nil { @@ -95,15 +100,15 @@ func (client *appClient) Exists() (bool, error) { return false, nil } -func (client *appClient) Create() error { +func (client *appClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.projectClient.backendProjectClient() if err != nil { - return err + return } externalID, err := client.externalID(client.app.Catalog, client.app.CatalogType, client.app.Chart, client.app.Version) if err != nil { - return err + return } client.logger.Info("Create new app") pattern := &backendProjectClient.App{ @@ -116,11 +121,16 @@ func (client *appClient) Create() error { } else { pattern.Answers = client.app.Answers } - _, err = backendClient.App.Create(pattern) - return err + + if dryRun { + client.logger.WithField("object", pattern).Info("Do Dry-Run Create") + } else { + _, err = backendClient.App.Create(pattern) + } + return err == nil, err } -func (client *appClient) Upgrade() (err error) { +func (client *appClient) Upgrade(dryRun bool) (changed bool, err error) { backendClient, err := client.projectClient.backendProjectClient() if err != nil { @@ -129,12 +139,12 @@ func (client *appClient) Upgrade() (err error) { installedApp, err := client.loadInstalledApp() if installedApp == nil { - return fmt.Errorf("App %v not found", client.name) + return changed, fmt.Errorf("App %v not found", client.name) } externalID, err := client.externalID(client.app.Catalog, client.app.CatalogType, client.app.Chart, client.app.Version) if err != nil { - return err + return } au := &backendProjectClient.AppUpgradeConfig{ ExternalID: externalID, @@ -142,7 +152,7 @@ func (client *appClient) Upgrade() (err error) { if client.app.ValuesYaml != "" { if strings.TrimSpace(installedApp.ValuesYaml) == strings.TrimSpace(client.app.ValuesYaml) { client.logger.Debug("Skip upgrade app - no changes") - return nil + return } au.ValuesYaml = client.app.ValuesYaml } else { @@ -157,26 +167,40 @@ func (client *appClient) Upgrade() (err error) { } if reflect.DeepEqual(installedApp.Answers, resultAnswers) { client.logger.Debug("Skip upgrade app - no changes") - return nil + return } au.Answers = resultAnswers } if client.app.SkipUpgrade { client.logger.Info("Suppress upgrade app - by config") - return nil + return } client.logger.Info("Upgrade app") - return backendClient.App.ActionUpgrade(installedApp, au) + + if dryRun { + client.logger.WithField("object", installedApp).Info("Do Dry-Run Upgrade") + } else { + err = backendClient.App.ActionUpgrade(installedApp, au) + } + + return err == nil, err } -func (client *appClient) Delete() (err error) { +func (client *appClient) Delete(dryRun bool) (changed bool, err error) { backendClient, err := client.projectClient.backendProjectClient() if err != nil { return } installedApp, err := client.loadInstalledApp() - return backendClient.App.Delete(installedApp) + + if dryRun { + client.logger.WithField("object", installedApp).Info("Do Dry-Run Delete") + } else { + err = backendClient.App.Delete(installedApp) + } + + return err == nil, err } func (client *appClient) Data() (projectModel.App, error) { diff --git a/internal/pkg/rancher/client/app_client_test.go b/internal/pkg/rancher/client/app_client_test.go index 152b10b..9d0a2cc 100644 --- a/internal/pkg/rancher/client/app_client_test.go +++ b/internal/pkg/rancher/client/app_client_test.go @@ -144,7 +144,7 @@ func Test_appClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { @@ -240,7 +240,7 @@ key: value } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Upgrade() + _, err := tt.client.Upgrade(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/certificate_client.go b/internal/pkg/rancher/client/certificate_client.go index 414bfba..577100c 100644 --- a/internal/pkg/rancher/client/certificate_client.go +++ b/internal/pkg/rancher/client/certificate_client.go @@ -19,6 +19,7 @@ import ( "strings" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendProjectClient "github.com/rancher/types/client/project/v3" "github.com/sirupsen/logrus" @@ -69,6 +70,10 @@ type certificateClient struct { certificate projectModel.Certificate } +func (client *certificateClient) Type() string { + return rancherModel.Certificate +} + func (client *certificateClient) Exists() (bool, error) { if client.namespace != "" { return client.existsInNamespace() @@ -127,25 +132,25 @@ func (client *certificateClient) existsInNamespace() (bool, error) { return false, nil } -func (client *certificateClient) Create() error { +func (client *certificateClient) Create(dryRun bool) (changed bool, err error) { projectID, err := client.project.ID() if err != nil { - return fmt.Errorf("Failed to read namespace ID, %v", err) + return changed, fmt.Errorf("Failed to read namespace ID, %v", err) } client.logger.Info("Create new Certificate") labels := make(map[string]string) labels["cattlectl.io/hash"] = hashOf(client.certificate) if client.namespace != "" { - return client.createInNamespace(projectID, labels) + return client.createInNamespace(projectID, labels, dryRun) } - return client.createInProject(projectID, labels) + return client.createInProject(projectID, labels, dryRun) } -func (client *certificateClient) createInProject(projectID string, labels map[string]string) error { +func (client *certificateClient) createInProject(projectID string, labels map[string]string, dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } newCertificate := &backendProjectClient.Certificate{ Name: client.certificate.Name, @@ -155,18 +160,22 @@ func (client *certificateClient) createInProject(projectID string, labels map[st ProjectID: projectID, } - _, err = backendClient.Certificate.Create(newCertificate) - return err + if dryRun { + client.logger.WithField("object", newCertificate).Info("Do Dry-Run Create") + } else { + _, err = backendClient.Certificate.Create(newCertificate) + } + return err == nil, err } -func (client *certificateClient) createInNamespace(projectID string, labels map[string]string) error { +func (client *certificateClient) createInNamespace(projectID string, labels map[string]string, dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } newCertificate := &backendProjectClient.NamespacedCertificate{ Name: client.certificate.Name, @@ -176,21 +185,26 @@ func (client *certificateClient) createInNamespace(projectID string, labels map[ NamespaceId: namespaceID, ProjectID: projectID, } - _, err = backendClient.NamespacedCertificate.Create(newCertificate) - return err + + if dryRun { + client.logger.WithField("object", newCertificate).Info("Do Dry-Run Create") + } else { + _, err = backendClient.NamespacedCertificate.Create(newCertificate) + } + return err == nil, err } -func (client *certificateClient) Upgrade() error { +func (client *certificateClient) Upgrade(dryRun bool) (changed bool, err error) { if client.namespace != "" { - return client.upgradeInNamespace() + return client.upgradeInNamespace(dryRun) } - return client.upgradeInProject() + return client.upgradeInProject(dryRun) } -func (client *certificateClient) upgradeInProject() error { +func (client *certificateClient) upgradeInProject(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } collection, err := backendClient.Certificate.List(&types.ListOpts{ Filters: map[string]interface{}{ @@ -199,33 +213,37 @@ func (client *certificateClient) upgradeInProject() error { }) if nil != err { client.logger.WithError(err).Error("Failed to read certificate list") - return fmt.Errorf("Failed to read certificate list, %v", err) + return changed, fmt.Errorf("Failed to read certificate list, %v", err) } if len(collection.Data) == 0 { - return fmt.Errorf("Certificate %v not found", client.name) + return changed, fmt.Errorf("Certificate %v not found", client.name) } existingCertificate := collection.Data[0] if strings.TrimSpace(existingCertificate.Certs) == strings.TrimSpace(client.certificate.Certs) { client.logger.Debug("Skip upgrade certificate - no changes") - return nil + return } client.logger.Info("Upgrade Certificate") existingCertificate.Key = client.certificate.Key existingCertificate.Certs = client.certificate.Certs - _, err = backendClient.Certificate.Replace(&existingCertificate) - return err + if dryRun { + client.logger.WithField("object", existingCertificate).Info("Do Dry-Run Upgrade") + } else { + _, err = backendClient.Certificate.Replace(&existingCertificate) + } + return err == nil, err } -func (client *certificateClient) upgradeInNamespace() error { +func (client *certificateClient) upgradeInNamespace(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } collection, err := backendClient.NamespacedCertificate.List(&types.ListOpts{ Filters: map[string]interface{}{ @@ -235,23 +253,27 @@ func (client *certificateClient) upgradeInNamespace() error { }) if nil != err { client.logger.WithError(err).Error("Failed to read certificate list") - return fmt.Errorf("Failed to read certificate list, %v", err) + return changed, fmt.Errorf("Failed to read certificate list, %v", err) } if len(collection.Data) == 0 { - return fmt.Errorf("Certificate %v not found", client.name) + return changed, fmt.Errorf("Certificate %v not found", client.name) } existingCertificate := collection.Data[0] if strings.TrimSpace(existingCertificate.Certs) == strings.TrimSpace(client.certificate.Certs) { client.logger.Debug("Skip upgrade certificate - no changes") - return nil + return } client.logger.Info("Upgrade Certificate") existingCertificate.Key = client.certificate.Key existingCertificate.Certs = client.certificate.Certs - _, err = backendClient.NamespacedCertificate.Replace(&existingCertificate) - return err + if dryRun { + client.logger.WithField("object", existingCertificate).Info("Do Dry-Run Upgrade") + } else { + _, err = backendClient.NamespacedCertificate.Replace(&existingCertificate) + } + return err == nil, err } func (client *certificateClient) Data() (projectModel.Certificate, error) { diff --git a/internal/pkg/rancher/client/certificate_client_test.go b/internal/pkg/rancher/client/certificate_client_test.go index da27960..5c4cf0e 100644 --- a/internal/pkg/rancher/client/certificate_client_test.go +++ b/internal/pkg/rancher/client/certificate_client_test.go @@ -143,7 +143,7 @@ func Test_certificateClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { @@ -188,7 +188,7 @@ func Test_certificateClient_Upgrade(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Upgrade() + _, err := tt.client.Upgrade(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/clients.go b/internal/pkg/rancher/client/clients.go index 0413929..556447a 100644 --- a/internal/pkg/rancher/client/clients.go +++ b/internal/pkg/rancher/client/clients.go @@ -35,11 +35,12 @@ type RancherClient interface { // ResourceClient is a client to any Rancher resource type ResourceClient interface { ID() (string, error) + Type() string Name() (string, error) Exists() (bool, error) - Create() error - Upgrade() error - Delete() error + Create(bool) (bool, error) + Upgrade(bool) (bool, error) + Delete(bool) (bool, error) } // NamespacedResourceClient is a client to any Rancher resource belonging to a namespace diff --git a/internal/pkg/rancher/client/cluster_catalog_client.go b/internal/pkg/rancher/client/cluster_catalog_client.go index f82d0ff..a765fa0 100644 --- a/internal/pkg/rancher/client/cluster_catalog_client.go +++ b/internal/pkg/rancher/client/cluster_catalog_client.go @@ -60,6 +60,10 @@ type clusterCatalogClient struct { clusterClient ClusterClient } +func (client *clusterCatalogClient) Type() string { + return rancherModel.ClusterCatalog +} + func (client *clusterCatalogClient) Exists() (bool, error) { backendClient, err := client.clusterClient.backendRancherClient() if err != nil { @@ -88,17 +92,17 @@ func (client *clusterCatalogClient) Exists() (bool, error) { return false, nil } -func (client *clusterCatalogClient) Create() error { +func (client *clusterCatalogClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.clusterClient.backendRancherClient() if err != nil { - return err + return } clusterID, err := client.clusterClient.ID() if err != nil { - return err + return } client.logger.Info("Create new catalog") - _, err = backendClient.ClusterCatalog.Create(&backendRancherClient.ClusterCatalog{ + newClusterCatalog := &backendRancherClient.ClusterCatalog{ Name: client.catalog.Name, ClusterID: clusterID, URL: client.catalog.URL, @@ -108,18 +112,24 @@ func (client *clusterCatalogClient) Create() error { Labels: map[string]string{ "cattlectl.io/hash": hashOf(client.catalog), }, - }) - return err + } + + if dryRun { + client.logger.WithField("object", newClusterCatalog).Info("Do Dry-Run Create") + } else { + _, err = backendClient.ClusterCatalog.Create(newClusterCatalog) + } + return err == nil, err } -func (client *clusterCatalogClient) Upgrade() error { +func (client *clusterCatalogClient) Upgrade(dryRun bool) (changed bool, err error) { backendClient, err := client.clusterClient.backendRancherClient() if err != nil { - return err + return } clusterID, err := client.clusterClient.ID() if err != nil { - return err + return } client.logger.Trace("Load from rancher") collection, err := backendClient.ClusterCatalog.List(&types.ListOpts{ @@ -130,17 +140,17 @@ func (client *clusterCatalogClient) Upgrade() error { }) if nil != err { client.logger.WithError(err).Error("Failed to read catalog list") - return fmt.Errorf("Failed to read catalog list, %v", err) + return changed, fmt.Errorf("Failed to read catalog list, %v", err) } if len(collection.Data) == 0 { - return fmt.Errorf("Catalog %v not found", client.name) + return changed, fmt.Errorf("Catalog %v not found", client.name) } existingCatalog := collection.Data[0] if isClusterCatalogUnchanged(existingCatalog, client.catalog) { client.logger.Debug("Skip upgrade catalog - no changes") - return nil + return } client.logger.Info("Upgrade ClusterCatalog") existingCatalog.Labels["cattlectl.io/hash"] = hashOf(client.catalog) @@ -149,8 +159,12 @@ func (client *clusterCatalogClient) Upgrade() error { existingCatalog.Username = client.catalog.Username existingCatalog.Password = client.catalog.Password - _, err = backendClient.ClusterCatalog.Replace(&existingCatalog) - return err + if dryRun { + client.logger.WithField("object", existingCatalog).Info("Do Dry-Run Upgrade") + } else { + _, err = backendClient.ClusterCatalog.Replace(&existingCatalog) + } + return err == nil, err } func (client *clusterCatalogClient) Data() (rancherModel.Catalog, error) { diff --git a/internal/pkg/rancher/client/cluster_catalog_client_test.go b/internal/pkg/rancher/client/cluster_catalog_client_test.go index a6d5d14..1dc8093 100644 --- a/internal/pkg/rancher/client/cluster_catalog_client_test.go +++ b/internal/pkg/rancher/client/cluster_catalog_client_test.go @@ -100,7 +100,7 @@ func Test_clusterCatalogClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { @@ -133,7 +133,7 @@ func Test_clusterCatalogClient_Upgrade(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Upgrade() + _, err := tt.client.Upgrade(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/cluster_client.go b/internal/pkg/rancher/client/cluster_client.go index c0ad1b3..e810119 100644 --- a/internal/pkg/rancher/client/cluster_client.go +++ b/internal/pkg/rancher/client/cluster_client.go @@ -18,6 +18,7 @@ import ( "fmt" clusterModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendClusterClient "github.com/rancher/types/client/cluster/v3" backendRancherClient "github.com/rancher/types/client/management/v3" @@ -63,6 +64,10 @@ type namespaceCacheEntry struct { namespace NamespaceClient } +func (client *clusterClient) Type() string { + return rancherModel.Cluster +} + func (client *clusterClient) init() error { if client._backendClusterClient != nil { return nil @@ -103,7 +108,7 @@ func (client *clusterClient) ID() (string, error) { func (client *clusterClient) Exists() (bool, error) { _, err := client.ID() - return err != nil, err + return err == nil, err } func (client *clusterClient) Project(name string) (ProjectClient, error) { if cache, exists := client.projectClients[name]; exists { diff --git a/internal/pkg/rancher/client/config_map_client.go b/internal/pkg/rancher/client/config_map_client.go index 4d4ca34..6428ecc 100644 --- a/internal/pkg/rancher/client/config_map_client.go +++ b/internal/pkg/rancher/client/config_map_client.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendProjectClient "github.com/rancher/types/client/project/v3" "github.com/sirupsen/logrus" @@ -64,6 +65,10 @@ type configMapClient struct { configMap projectModel.ConfigMap } +func (client *configMapClient) Type() string { + return rancherModel.ConfigMap +} + func (client *configMapClient) Exists() (bool, error) { backendClient, err := client.project.backendProjectClient() if err != nil { @@ -92,18 +97,18 @@ func (client *configMapClient) Exists() (bool, error) { return false, nil } -func (client *configMapClient) Create() error { +func (client *configMapClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } projectID, err := client.project.ID() if err != nil { - return fmt.Errorf("Failed to read namespace ID, %v", err) + return changed, fmt.Errorf("Failed to read namespace ID, %v", err) } client.logger.Info("Create new ConfigMap") labels := make(map[string]string) @@ -116,18 +121,22 @@ func (client *configMapClient) Create() error { ProjectID: projectID, } - _, err = backendClient.ConfigMap.Create(newConfigMap) - return err + if dryRun { + client.logger.WithField("object", newConfigMap).Info("Do Dry-Run Create") + } else { + _, err = backendClient.ConfigMap.Create(newConfigMap) + } + return err == nil, err } -func (client *configMapClient) Upgrade() error { +func (client *configMapClient) Upgrade(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } collection, err := backendClient.ConfigMap.List(&types.ListOpts{ Filters: map[string]interface{}{ @@ -137,23 +146,27 @@ func (client *configMapClient) Upgrade() error { }) if nil != err { client.logger.WithError(err).Error("Failed to read configMap list") - return fmt.Errorf("Failed to read configMap list, %v", err) + return changed, fmt.Errorf("Failed to read configMap list, %v", err) } if len(collection.Data) == 0 { - return fmt.Errorf("ConfigMap %v not found", client.name) + return changed, fmt.Errorf("ConfigMap %v not found", client.name) } existingConfigMap := collection.Data[0] if isConfigMapUnchanged(existingConfigMap, client.configMap) { client.logger.Debug("Skip upgrade configMap - no changes") - return nil + return } client.logger.Info("Upgrade ConfigMap") existingConfigMap.Labels["cattlectl.io/hash"] = hashOf(client.configMap) existingConfigMap.Data = client.configMap.Data - _, err = backendClient.ConfigMap.Replace(&existingConfigMap) - return err + if dryRun { + client.logger.WithField("object", existingConfigMap).Info("Do Dry-Run Upgrade") + } else { + _, err = backendClient.ConfigMap.Replace(&existingConfigMap) + } + return err == nil, err } func (client *configMapClient) Data() (projectModel.ConfigMap, error) { diff --git a/internal/pkg/rancher/client/config_map_client_test.go b/internal/pkg/rancher/client/config_map_client_test.go index 434fc3c..60122ef 100644 --- a/internal/pkg/rancher/client/config_map_client_test.go +++ b/internal/pkg/rancher/client/config_map_client_test.go @@ -99,7 +99,7 @@ func Test_configMapClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/cronjob_client.go b/internal/pkg/rancher/client/cronjob_client.go index 0943d1d..70deca5 100644 --- a/internal/pkg/rancher/client/cronjob_client.go +++ b/internal/pkg/rancher/client/cronjob_client.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" "github.com/sirupsen/logrus" ) @@ -63,6 +64,10 @@ type cronJobClient struct { cronJob projectModel.CronJob } +func (client *cronJobClient) Type() string { + return rancherModel.CronJobKind +} + func (client *cronJobClient) Exists() (bool, error) { backendClient, err := client.project.backendProjectClient() if err != nil { @@ -91,28 +96,33 @@ func (client *cronJobClient) Exists() (bool, error) { return false, nil } -func (client *cronJobClient) Create() error { +func (client *cronJobClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } client.logger.Info("Create new cronJob") pattern, err := projectModel.ConvertCronJobToProjectAPI(client.cronJob) if err != nil { - return err + return } pattern.NamespaceId = namespaceID - _, err = backendClient.CronJob.Create(&pattern) - return err + + if dryRun { + client.logger.WithField("object", pattern).Info("Do Dry-Run Create") + } else { + _, err = backendClient.CronJob.Create(&pattern) + } + return err == nil, err } -func (client *cronJobClient) Upgrade() error { +func (client *cronJobClient) Upgrade(dryRun bool) (changed bool, err error) { client.logger.Warn("Skip change existing cronjob") - return nil + return } func (client *cronJobClient) Data() (projectModel.CronJob, error) { diff --git a/internal/pkg/rancher/client/cronjob_client_test.go b/internal/pkg/rancher/client/cronjob_client_test.go index b267583..dcd5a1d 100644 --- a/internal/pkg/rancher/client/cronjob_client_test.go +++ b/internal/pkg/rancher/client/cronjob_client_test.go @@ -99,7 +99,7 @@ func Test_cronJobClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/daemon_set_client.go b/internal/pkg/rancher/client/daemon_set_client.go index 60c3477..7ce9182 100644 --- a/internal/pkg/rancher/client/daemon_set_client.go +++ b/internal/pkg/rancher/client/daemon_set_client.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" "github.com/sirupsen/logrus" ) @@ -63,6 +64,10 @@ type daemonSetClient struct { daemonSet projectModel.DaemonSet } +func (client *daemonSetClient) Type() string { + return rancherModel.DaemonSetKind +} + func (client *daemonSetClient) Exists() (bool, error) { backendClient, err := client.project.backendProjectClient() if err != nil { @@ -91,28 +96,33 @@ func (client *daemonSetClient) Exists() (bool, error) { return false, nil } -func (client *daemonSetClient) Create() error { +func (client *daemonSetClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } client.logger.Info("Create new daemonSet") pattern, err := projectModel.ConvertDaemonSetToProjectAPI(client.daemonSet) if err != nil { - return err + return } pattern.NamespaceId = namespaceID - _, err = backendClient.DaemonSet.Create(&pattern) - return err + + if dryRun { + client.logger.WithField("object", pattern).Info("Do Dry-Run Create") + } else { + _, err = backendClient.DaemonSet.Create(&pattern) + } + return err == nil, err } -func (client *daemonSetClient) Upgrade() error { +func (client *daemonSetClient) Upgrade(dryRun bool) (changed bool, err error) { client.logger.Warn("Skip change existing daemonset") - return nil + return } func (client *daemonSetClient) Data() (projectModel.DaemonSet, error) { diff --git a/internal/pkg/rancher/client/daemon_set_client_test.go b/internal/pkg/rancher/client/daemon_set_client_test.go index 51626b4..7f50751 100644 --- a/internal/pkg/rancher/client/daemon_set_client_test.go +++ b/internal/pkg/rancher/client/daemon_set_client_test.go @@ -99,7 +99,7 @@ func Test_daemonSetClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/deployment.go b/internal/pkg/rancher/client/deployment.go index 08499d3..1b89269 100644 --- a/internal/pkg/rancher/client/deployment.go +++ b/internal/pkg/rancher/client/deployment.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" "github.com/sirupsen/logrus" ) @@ -63,6 +64,10 @@ type deploymentClient struct { deployment projectModel.Deployment } +func (client *deploymentClient) Type() string { + return rancherModel.DeploymentKind +} + func (client *deploymentClient) Exists() (bool, error) { backendClient, err := client.project.backendProjectClient() if err != nil { @@ -91,28 +96,33 @@ func (client *deploymentClient) Exists() (bool, error) { return false, nil } -func (client *deploymentClient) Create() error { +func (client *deploymentClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } client.logger.Info("Create new deployment") pattern, err := projectModel.ConvertDeploymentToProjectAPI(client.deployment) if err != nil { - return err + return } pattern.NamespaceId = namespaceID - _, err = backendClient.Deployment.Create(&pattern) - return err + + if dryRun { + client.logger.WithField("object", pattern).Info("Do Dry-Run Create") + } else { + _, err = backendClient.Deployment.Create(&pattern) + } + return err == nil, err } -func (client *deploymentClient) Upgrade() error { +func (client *deploymentClient) Upgrade(dryRun bool) (changed bool, err error) { client.logger.Warn("Skip change existing deployment") - return nil + return } func (client *deploymentClient) Data() (projectModel.Deployment, error) { diff --git a/internal/pkg/rancher/client/deployment_test.go b/internal/pkg/rancher/client/deployment_test.go index 786c022..6cc5ae4 100644 --- a/internal/pkg/rancher/client/deployment_test.go +++ b/internal/pkg/rancher/client/deployment_test.go @@ -99,7 +99,7 @@ func Test_deploymentClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/docker_credential_client.go b/internal/pkg/rancher/client/docker_credential_client.go index c353796..c211280 100644 --- a/internal/pkg/rancher/client/docker_credential_client.go +++ b/internal/pkg/rancher/client/docker_credential_client.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendProjectClient "github.com/rancher/types/client/project/v3" "github.com/sirupsen/logrus" @@ -64,6 +65,10 @@ type dockerCredentialClient struct { dockerCredential projectModel.DockerCredential } +func (client *dockerCredentialClient) Type() string { + return rancherModel.DockerCredential +} + func (client *dockerCredentialClient) Exists() (bool, error) { if client.namespace != "" { return client.existsInNamespace() @@ -122,7 +127,7 @@ func (client *dockerCredentialClient) existsInNamespace() (bool, error) { return false, nil } -func (client *dockerCredentialClient) Create() error { +func (client *dockerCredentialClient) Create(dryRun bool) (changed bool, err error) { client.logger.Info("Create new DockerCredential") registries := make(map[string]backendProjectClient.RegistryCredential) @@ -136,18 +141,18 @@ func (client *dockerCredentialClient) Create() error { labels["cattlectl.io/hash"] = hashOf(client.dockerCredential) projectID, err := client.project.ID() if err != nil { - return err + return } if client.namespace != "" { - return client.createInNamespace(registries, labels, projectID) + return client.createInNamespace(registries, labels, projectID, dryRun) } - return client.createInProject(registries, labels, projectID) + return client.createInProject(registries, labels, projectID, dryRun) } -func (client *dockerCredentialClient) createInProject(registries map[string]backendProjectClient.RegistryCredential, labels map[string]string, projectID string) error { +func (client *dockerCredentialClient) createInProject(registries map[string]backendProjectClient.RegistryCredential, labels map[string]string, projectID string, dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } newDockerCredential := &backendProjectClient.DockerCredential{ Name: client.dockerCredential.Name, @@ -157,18 +162,22 @@ func (client *dockerCredentialClient) createInProject(registries map[string]back ProjectID: projectID, } - _, err = backendClient.DockerCredential.Create(newDockerCredential) - return err + if dryRun { + client.logger.WithField("object", newDockerCredential).Info("Do Dry-Run Create") + } else { + _, err = backendClient.DockerCredential.Create(newDockerCredential) + } + return err == nil, err } -func (client *dockerCredentialClient) createInNamespace(registries map[string]backendProjectClient.RegistryCredential, labels map[string]string, projectID string) error { +func (client *dockerCredentialClient) createInNamespace(registries map[string]backendProjectClient.RegistryCredential, labels map[string]string, projectID string, dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } newDockerCredential := &backendProjectClient.NamespacedDockerCredential{ Name: client.dockerCredential.Name, @@ -178,21 +187,25 @@ func (client *dockerCredentialClient) createInNamespace(registries map[string]ba ProjectID: projectID, } - _, err = backendClient.NamespacedDockerCredential.Create(newDockerCredential) - return err + if dryRun { + client.logger.WithField("object", newDockerCredential).Info("Do Dry-Run Create") + } else { + _, err = backendClient.NamespacedDockerCredential.Create(newDockerCredential) + } + return err == nil, err } -func (client *dockerCredentialClient) Upgrade() error { +func (client *dockerCredentialClient) Upgrade(dryRun bool) (changed bool, err error) { if client.namespace != "" { - return client.upgradeInNamespace() + return client.upgradeInNamespace(dryRun) } - return client.upgradeInProject() + return client.upgradeInProject(dryRun) } -func (client *dockerCredentialClient) upgradeInProject() error { +func (client *dockerCredentialClient) upgradeInProject(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } collection, err := backendClient.DockerCredential.List(&types.ListOpts{ Filters: map[string]interface{}{ @@ -201,16 +214,16 @@ func (client *dockerCredentialClient) upgradeInProject() error { }) if nil != err { client.logger.WithError(err).Error("Failed to read dockerCredential list") - return fmt.Errorf("Failed to read dockerCredential list, %v", err) + return changed, fmt.Errorf("Failed to read dockerCredential list, %v", err) } if len(collection.Data) == 0 { - return fmt.Errorf("DockerCredential %v not found", client.name) + return changed, fmt.Errorf("DockerCredential %v not found", client.name) } existingDockerCredential := collection.Data[0] if isProjectDockerCredentialUnchanged(existingDockerCredential, client.dockerCredential) { client.logger.Debug("Skip upgrade DockerCredential - no changes") - return nil + return } client.logger.Info("Upgrade DockerCredential") registries := make(map[string]backendProjectClient.RegistryCredential) @@ -223,18 +236,22 @@ func (client *dockerCredentialClient) upgradeInProject() error { existingDockerCredential.Registries = registries existingDockerCredential.Labels["cattlectl.io/hash"] = hashOf(client.dockerCredential) - _, err = backendClient.DockerCredential.Replace(&existingDockerCredential) - return err + if dryRun { + client.logger.WithField("object", existingDockerCredential).Info("Do Dry-Run Upgrade") + } else { + _, err = backendClient.DockerCredential.Replace(&existingDockerCredential) + } + return err == nil, err } -func (client *dockerCredentialClient) upgradeInNamespace() error { +func (client *dockerCredentialClient) upgradeInNamespace(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } collection, err := backendClient.NamespacedDockerCredential.List(&types.ListOpts{ Filters: map[string]interface{}{ @@ -244,16 +261,16 @@ func (client *dockerCredentialClient) upgradeInNamespace() error { }) if nil != err { client.logger.WithError(err).Error("Failed to read dockerCredential list") - return fmt.Errorf("Failed to read dockerCredential list, %v", err) + return changed, fmt.Errorf("Failed to read dockerCredential list, %v", err) } if len(collection.Data) == 0 { - return fmt.Errorf("DockerCredential %v not found", client.name) + return changed, fmt.Errorf("DockerCredential %v not found", client.name) } existingDockerCredential := collection.Data[0] if isNamespacedDockerCredentialUnchanged(existingDockerCredential, client.dockerCredential) { client.logger.Debug("Skip upgrade DockerCredential - no changes") - return nil + return } client.logger.Info("Upgrade DockerCredential") registries := make(map[string]backendProjectClient.RegistryCredential) @@ -266,8 +283,12 @@ func (client *dockerCredentialClient) upgradeInNamespace() error { existingDockerCredential.Registries = registries existingDockerCredential.Labels["cattlectl.io/hash"] = hashOf(client.dockerCredential) - _, err = backendClient.NamespacedDockerCredential.Replace(&existingDockerCredential) - return err + if dryRun { + client.logger.WithField("object", existingDockerCredential).Info("Do Dry-Run Upgrade") + } else { + _, err = backendClient.NamespacedDockerCredential.Replace(&existingDockerCredential) + } + return err == nil, err } func (client *dockerCredentialClient) Data() (projectModel.DockerCredential, error) { diff --git a/internal/pkg/rancher/client/docker_credential_client_test.go b/internal/pkg/rancher/client/docker_credential_client_test.go index 737208e..aea4855 100644 --- a/internal/pkg/rancher/client/docker_credential_client_test.go +++ b/internal/pkg/rancher/client/docker_credential_client_test.go @@ -99,7 +99,7 @@ func Test_dockerCredentialClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { @@ -132,7 +132,7 @@ func Test_dockerCredentialClient_Upgrade(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Upgrade() + _, err := tt.client.Upgrade(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/job_client.go b/internal/pkg/rancher/client/job_client.go index b6dbadd..a0f6e08 100644 --- a/internal/pkg/rancher/client/job_client.go +++ b/internal/pkg/rancher/client/job_client.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendProjectClient "github.com/rancher/types/client/project/v3" "github.com/sirupsen/logrus" @@ -64,6 +65,10 @@ type jobClient struct { job projectModel.Job } +func (client *jobClient) Type() string { + return rancherModel.JobKind +} + func (client *jobClient) Exists() (bool, error) { backendClient, err := client.project.backendProjectClient() if err != nil { @@ -92,37 +97,49 @@ func (client *jobClient) Exists() (bool, error) { return false, nil } -func (client *jobClient) Create() error { +func (client *jobClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } client.logger.Info("Create new job") pattern, err := projectModel.ConvertJobToProjectAPI(client.job) if err != nil { - return err + return } pattern.NamespaceId = namespaceID - _, err = backendClient.Job.Create(&pattern) - return err + + if dryRun { + client.logger.WithField("object", pattern).Info("Do Dry-Run Create") + } else { + _, err = backendClient.Job.Create(&pattern) + } + return err == nil, err } -func (client *jobClient) Upgrade() error { +func (client *jobClient) Upgrade(dryRun bool) (changed bool, err error) { client.logger.Warn("Skip change existing job") - return nil + return } -func (client *jobClient) Delete() (err error) { +func (client *jobClient) Delete(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { return } installedJob, err := client.loadExistingJob() - return backendClient.Job.Delete(installedJob) + + if dryRun { + client.logger.WithField("object", installedJob).Info("Do Dry-Run Delete") + changed = true + return + } + err = backendClient.Job.Delete(installedJob) + return err == nil, err } func (client *jobClient) Data() (projectModel.Job, error) { diff --git a/internal/pkg/rancher/client/job_client_test.go b/internal/pkg/rancher/client/job_client_test.go index 4558adf..56742c8 100644 --- a/internal/pkg/rancher/client/job_client_test.go +++ b/internal/pkg/rancher/client/job_client_test.go @@ -99,7 +99,7 @@ func Test_jobClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/namespace_client.go b/internal/pkg/rancher/client/namespace_client.go index 040c06e..b9d2db2 100644 --- a/internal/pkg/rancher/client/namespace_client.go +++ b/internal/pkg/rancher/client/namespace_client.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendClusterClient "github.com/rancher/types/client/cluster/v3" "github.com/sirupsen/logrus" @@ -65,6 +66,10 @@ type namespaceClient struct { clusterClient ClusterClient } +func (client *namespaceClient) Type() string { + return rancherModel.Namespace +} + func (client *namespaceClient) ID() (string, error) { if client.id != "" { return client.id, nil @@ -113,10 +118,10 @@ func (client *namespaceClient) Exists() (bool, error) { return false, nil } -func (client *namespaceClient) Create() error { +func (client *namespaceClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.clusterClient.backendClusterClient() if err != nil { - return err + return } client.logger.Info("Create new namespace") @@ -126,30 +131,41 @@ func (client *namespaceClient) Create() error { if hasProjct, _ := client.HasProject(); hasProjct { projectID, err := client.projectClient.ID() if err != nil { - return err + return changed, err } newNamespace.ProjectID = projectID } - _, err = backendClient.Namespace.Create(newNamespace) - return err + if dryRun { + client.logger.WithField("object", newNamespace).Info("Do Dry-Run Create") + } else { + _, err = backendClient.Namespace.Create(newNamespace) + } + return err == nil, err } -func (client *namespaceClient) Upgrade() error { +func (client *namespaceClient) Upgrade(dryRun bool) (changed bool, err error) { client.logger.Debug("Skip change existing namespace") - return nil + return } -func (client *namespaceClient) Delete() (err error) { +func (client *namespaceClient) Delete(dryRun bool) (changed bool, err error) { backendClient, err := client.clusterClient.backendClusterClient() if err != nil { - return err + return } existingNamespace, err := client.loadExistingNamespace() if err != nil { - return err + return } - return backendClient.Namespace.Delete(existingNamespace) + + if dryRun { + client.logger.WithField("object", existingNamespace).Info("Do Dry-Run Delete") + } else { + err = backendClient.Namespace.Delete(existingNamespace) + } + + return err == nil, err } func (client *namespaceClient) HasProject() (bool, error) { diff --git a/internal/pkg/rancher/client/namespace_client_test.go b/internal/pkg/rancher/client/namespace_client_test.go index ee44095..c802b38 100644 --- a/internal/pkg/rancher/client/namespace_client_test.go +++ b/internal/pkg/rancher/client/namespace_client_test.go @@ -98,7 +98,7 @@ func Test_namespaceClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/persistent_volume_client.go b/internal/pkg/rancher/client/persistent_volume_client.go index 0ed1af3..5bcb005 100644 --- a/internal/pkg/rancher/client/persistent_volume_client.go +++ b/internal/pkg/rancher/client/persistent_volume_client.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendClusterClient "github.com/rancher/types/client/cluster/v3" "github.com/sirupsen/logrus" @@ -60,6 +61,10 @@ type persistentVolumeClient struct { clusterClient ClusterClient } +func (client *persistentVolumeClient) Type() string { + return rancherModel.PersistentVolume +} + func (client *persistentVolumeClient) Exists() (bool, error) { backendClient, err := client.clusterClient.backendClusterClient() if err != nil { @@ -83,10 +88,10 @@ func (client *persistentVolumeClient) Exists() (bool, error) { return false, nil } -func (client *persistentVolumeClient) Create() error { +func (client *persistentVolumeClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.clusterClient.backendClusterClient() if err != nil { - return err + return } client.logger.Info("Create new persistent volume") newPersistentVolume := &backendClusterClient.PersistentVolume{ @@ -115,13 +120,17 @@ func (client *persistentVolumeClient) Create() error { }, } - _, err = backendClient.PersistentVolume.Create(newPersistentVolume) - return err + if dryRun { + client.logger.WithField("object", newPersistentVolume).Info("Do Dry-Run Create") + } else { + _, err = backendClient.PersistentVolume.Create(newPersistentVolume) + } + return err == nil, err } -func (client *persistentVolumeClient) Upgrade() error { +func (client *persistentVolumeClient) Upgrade(dryRun bool) (changed bool, err error) { client.logger.Debug("Skip change existing persistent volume") - return nil + return } func (client *persistentVolumeClient) Data() (projectModel.PersistentVolume, error) { diff --git a/internal/pkg/rancher/client/persistent_volume_client_test.go b/internal/pkg/rancher/client/persistent_volume_client_test.go index 23acaac..f1c56a1 100644 --- a/internal/pkg/rancher/client/persistent_volume_client_test.go +++ b/internal/pkg/rancher/client/persistent_volume_client_test.go @@ -93,7 +93,7 @@ func Test_persistentVolumeClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/project_catalog_client.go b/internal/pkg/rancher/client/project_catalog_client.go index 01eb0b4..92e2034 100644 --- a/internal/pkg/rancher/client/project_catalog_client.go +++ b/internal/pkg/rancher/client/project_catalog_client.go @@ -60,6 +60,10 @@ type projectCatalogClient struct { projectClient ProjectClient } +func (client *projectCatalogClient) Type() string { + return rancherModel.ProjectCatalog +} + func (client *projectCatalogClient) Exists() (bool, error) { backendClient, err := client.projectClient.backendRancherClient() if err != nil { @@ -88,17 +92,17 @@ func (client *projectCatalogClient) Exists() (bool, error) { return false, nil } -func (client *projectCatalogClient) Create() error { +func (client *projectCatalogClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.projectClient.backendRancherClient() if err != nil { - return err + return } projectID, err := client.projectClient.ID() if err != nil { - return err + return } client.logger.Info("Create new catalog") - _, err = backendClient.ProjectCatalog.Create(&backendRancherClient.ProjectCatalog{ + newProjectCatalog := backendRancherClient.ProjectCatalog{ Name: client.catalog.Name, ProjectID: projectID, URL: client.catalog.URL, @@ -108,18 +112,24 @@ func (client *projectCatalogClient) Create() error { Labels: map[string]string{ "cattlectl.io/hash": hashOf(client.catalog), }, - }) - return err + } + + if dryRun { + client.logger.WithField("object", newProjectCatalog).Info("Do Dry-Run Create") + } else { + _, err = backendClient.ProjectCatalog.Create(&newProjectCatalog) + } + return err == nil, err } -func (client *projectCatalogClient) Upgrade() error { +func (client *projectCatalogClient) Upgrade(dryRun bool) (changed bool, err error) { backendClient, err := client.projectClient.backendRancherClient() if err != nil { - return err + return } projectID, err := client.projectClient.ID() if err != nil { - return err + return } client.logger.Trace("Load from rancher") collection, err := backendClient.ProjectCatalog.List(&types.ListOpts{ @@ -130,17 +140,17 @@ func (client *projectCatalogClient) Upgrade() error { }) if nil != err { client.logger.WithError(err).Error("Failed to read catalog list") - return fmt.Errorf("Failed to read catalog list, %v", err) + return changed, fmt.Errorf("Failed to read catalog list, %v", err) } if len(collection.Data) == 0 { - return fmt.Errorf("Catalog %v not found", client.name) + return changed, fmt.Errorf("Catalog %v not found", client.name) } existingCatalog := collection.Data[0] if isProjectCatalogUnchanged(existingCatalog, client.catalog) { client.logger.Debug("Skip upgrade catalog - no changes") - return nil + return } client.logger.Info("Upgrade ProjectCatalog") existingCatalog.Labels["cattlectl.io/hash"] = hashOf(client.catalog) @@ -149,8 +159,12 @@ func (client *projectCatalogClient) Upgrade() error { existingCatalog.Username = client.catalog.Username existingCatalog.Password = client.catalog.Password - _, err = backendClient.ProjectCatalog.Replace(&existingCatalog) - return err + if dryRun { + client.logger.WithField("object", existingCatalog).Info("Do Dry-Run Upgrade") + } else { + _, err = backendClient.ProjectCatalog.Replace(&existingCatalog) + } + return err == nil, err } func (client *projectCatalogClient) Data() (rancherModel.Catalog, error) { diff --git a/internal/pkg/rancher/client/project_catalog_client_test.go b/internal/pkg/rancher/client/project_catalog_client_test.go index d0cbfa9..0d35630 100644 --- a/internal/pkg/rancher/client/project_catalog_client_test.go +++ b/internal/pkg/rancher/client/project_catalog_client_test.go @@ -100,7 +100,7 @@ func Test_projectCatalogClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { @@ -133,7 +133,7 @@ func Test_projectCatalogClient_Upgrade(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Upgrade() + _, err := tt.client.Upgrade(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/project_client.go b/internal/pkg/rancher/client/project_client.go index e1dab2a..717c38c 100644 --- a/internal/pkg/rancher/client/project_client.go +++ b/internal/pkg/rancher/client/project_client.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendClusterClient "github.com/rancher/types/client/cluster/v3" backendRancherClient "github.com/rancher/types/client/management/v3" @@ -73,6 +74,10 @@ type projectClient struct { catalogClients map[string]CatalogClient } +func (client *projectClient) Type() string { + return rancherModel.ProjectKind +} + func (client *projectClient) init() error { if client._backendProjectClient != nil { return nil @@ -129,32 +134,38 @@ func (client *projectClient) Exists() (bool, error) { projectID, err := client.ID() return projectID != "", err } -func (client *projectClient) Create() error { +func (client *projectClient) Create(dryRun bool) (changed bool, err error) { client.logger.Info("Create new project") backendClient, err := client.clusterClient.backendRancherClient() if err != nil { - return err + return } clusterID, err := client.clusterClient.ID() if err != nil { - return err + return } pattern := &backendRancherClient.Project{ ClusterID: clusterID, Name: client.name, } - createdProject, err := backendClient.Project.Create(pattern) - if err != nil { - client.logger.Warn("Failed to create project") - return err + + if dryRun { + client.logger.WithField("object", pattern).Info("Do Dry-Run Create") + client.id = client.name + } else { + createdProject, err := backendClient.Project.Create(pattern) + if err != nil { + client.logger.Warn("Failed to create project") + } else { + client.id = createdProject.ID + } } - client.id = createdProject.ID - return nil + return err == nil, err } -func (client *projectClient) Upgrade() error { +func (client *projectClient) Upgrade(dryRun bool) (changed bool, err error) { client.logger.Debug("Project exists") - return nil + return } func (client *projectClient) Namespace(name string) (NamespaceClient, error) { return client.clusterClient.Namespace(name, client.name) diff --git a/internal/pkg/rancher/client/project_client_test.go b/internal/pkg/rancher/client/project_client_test.go index fb1e21b..8ca3062 100644 --- a/internal/pkg/rancher/client/project_client_test.go +++ b/internal/pkg/rancher/client/project_client_test.go @@ -100,7 +100,7 @@ func Test_projectClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { @@ -135,7 +135,7 @@ func Test_projectClient_Upgrade(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Upgrade() + _, err := tt.client.Upgrade(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/rancher_catalog_client.go b/internal/pkg/rancher/client/rancher_catalog_client.go index 65df40c..6741dda 100644 --- a/internal/pkg/rancher/client/rancher_catalog_client.go +++ b/internal/pkg/rancher/client/rancher_catalog_client.go @@ -60,6 +60,10 @@ type rancherCatalogClient struct { rancherClient RancherClient } +func (client *rancherCatalogClient) Type() string { + return rancherModel.RancherCatalog +} + func (client *rancherCatalogClient) Exists() (bool, error) { backendClient, err := client.rancherClient.backendRancherClient() if err != nil { @@ -83,14 +87,14 @@ func (client *rancherCatalogClient) Exists() (bool, error) { return false, nil } -func (client *rancherCatalogClient) Create() error { +func (client *rancherCatalogClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.rancherClient.backendRancherClient() if err != nil { - return err + return } client.logger.Info("Create new catalog") - _, err = backendClient.Catalog.Create(&backendRancherClient.Catalog{ + newRancherCatalog := backendRancherClient.Catalog{ Name: client.catalog.Name, URL: client.catalog.URL, Branch: client.catalog.Branch, @@ -99,14 +103,20 @@ func (client *rancherCatalogClient) Create() error { Labels: map[string]string{ "cattlectl.io/hash": hashOf(client.catalog), }, - }) - return err + } + + if dryRun { + client.logger.WithField("object", newRancherCatalog).Info("Do Dry-Run Create") + } else { + _, err = backendClient.Catalog.Create(&newRancherCatalog) + } + return err == nil, err } -func (client *rancherCatalogClient) Upgrade() error { +func (client *rancherCatalogClient) Upgrade(dryRun bool) (changed bool, err error) { backendClient, err := client.rancherClient.backendRancherClient() if err != nil { - return err + return } client.logger.Trace("Load from rancher") collection, err := backendClient.Catalog.List(&types.ListOpts{ @@ -116,17 +126,17 @@ func (client *rancherCatalogClient) Upgrade() error { }) if nil != err { client.logger.WithError(err).Error("Failed to read catalog list") - return fmt.Errorf("Failed to read catalog list, %v", err) + return changed, fmt.Errorf("Failed to read catalog list, %v", err) } if len(collection.Data) == 0 { - return fmt.Errorf("Catalog %v not found", client.name) + return changed, fmt.Errorf("Catalog %v not found", client.name) } existingCatalog := collection.Data[0] if isRancherCatalogUnchanged(existingCatalog, client.catalog) { client.logger.Debug("Skip upgrade catalog - no changes") - return nil + return } client.logger.Info("Upgrade Catalog") existingCatalog.Labels["cattlectl.io/hash"] = hashOf(client.catalog) @@ -135,8 +145,12 @@ func (client *rancherCatalogClient) Upgrade() error { existingCatalog.Username = client.catalog.Username existingCatalog.Password = client.catalog.Password - _, err = backendClient.Catalog.Replace(&existingCatalog) - return err + if dryRun { + client.logger.WithField("object", existingCatalog).Info("Do Dry-Run Upgrade") + } else { + _, err = backendClient.Catalog.Replace(&existingCatalog) + } + return err == nil, err } func (client *rancherCatalogClient) Data() (rancherModel.Catalog, error) { diff --git a/internal/pkg/rancher/client/rancher_catalog_client_test.go b/internal/pkg/rancher/client/rancher_catalog_client_test.go index 1947d29..455267b 100644 --- a/internal/pkg/rancher/client/rancher_catalog_client_test.go +++ b/internal/pkg/rancher/client/rancher_catalog_client_test.go @@ -105,7 +105,7 @@ func Test_rancherCatalogClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { @@ -137,7 +137,7 @@ func Test_rancherCatalogClient_Upgrade(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Upgrade() + _, err := tt.client.Upgrade(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { @@ -224,7 +224,7 @@ func notExistingRancherCatalogClient(t *testing.T, name, url, branch, username, Branch: branch, Username: username, Password: password, - Labels: map[string]string{"cattlectl.io/hash": hashOf(rancherCatalogData)}, + Labels: map[string]string{"cattlectl.io/hash": hashOf(rancherCatalogData)}, } rancherCatalogOperationsStub := stubs.CreateRancherCatalogOperationsStub(t) diff --git a/internal/pkg/rancher/client/resource_client.go b/internal/pkg/rancher/client/resource_client.go index b8d2559..a786133 100644 --- a/internal/pkg/rancher/client/resource_client.go +++ b/internal/pkg/rancher/client/resource_client.go @@ -39,14 +39,14 @@ func (client *resourceClient) Name() (string, error) { func (client *resourceClient) Exists() (bool, error) { return false, fmt.Errorf("Exists not supported") } -func (client *resourceClient) Create() error { - return fmt.Errorf("Create not supported") +func (client *resourceClient) Create(dryRun bool) (changed bool, err error) { + return changed, fmt.Errorf("Create not supported") } -func (client *resourceClient) Upgrade() error { - return fmt.Errorf("Upgrade not supported") +func (client *resourceClient) Upgrade(dryRun bool) (changed bool, err error) { + return changed, fmt.Errorf("Upgrade not supported") } -func (client *resourceClient) Delete() error { - return fmt.Errorf("Delete not supported") +func (client *resourceClient) Delete(dryRun bool) (changed bool, err error) { + return changed, fmt.Errorf("Delete not supported") } type namespacedResourceClient struct { @@ -85,6 +85,9 @@ type emptyResourceClient struct{} func (client emptyResourceClient) ID() (string, error) { return "", nil } +func (client emptyResourceClient) Type() string { + return "EmptyResource" +} func (client emptyResourceClient) Name() (string, error) { return "", nil } @@ -92,12 +95,12 @@ func (client emptyResourceClient) Name() (string, error) { func (client emptyResourceClient) Exists() (bool, error) { return true, nil } -func (client emptyResourceClient) Create() error { - return nil +func (client emptyResourceClient) Create(dryRun bool) (changed bool, err error) { + return } -func (client emptyResourceClient) Upgrade() error { - return nil +func (client emptyResourceClient) Upgrade(dryRun bool) (changed bool, err error) { + return } -func (client emptyResourceClient) Delete() error { - return nil +func (client emptyResourceClient) Delete(dryRun bool) (changed bool, err error) { + return } diff --git a/internal/pkg/rancher/client/secret_client.go b/internal/pkg/rancher/client/secret_client.go index 04163d8..9c9be68 100644 --- a/internal/pkg/rancher/client/secret_client.go +++ b/internal/pkg/rancher/client/secret_client.go @@ -19,6 +19,7 @@ import ( "reflect" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendProjectClient "github.com/rancher/types/client/project/v3" "github.com/sirupsen/logrus" @@ -65,6 +66,10 @@ type secretClient struct { secret projectModel.ConfigMap } +func (client *secretClient) Type() string { + return rancherModel.Secret +} + func (client *secretClient) Exists() (bool, error) { if client.namespace != "" { return client.existsInNamespace() @@ -123,21 +128,21 @@ func (client *secretClient) existsInNamespace() (bool, error) { return false, nil } -func (client *secretClient) Create() error { +func (client *secretClient) Create(dryRun bool) (changed bool, err error) { if client.namespace != "" { - return client.createInNamespace() + return client.createInNamespace(dryRun) } - return client.createInProject() + return client.createInProject(dryRun) } -func (client *secretClient) createInProject() error { +func (client *secretClient) createInProject(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } projectID, err := client.project.ID() if err != nil { - return err + return } client.logger.Info("Create new Secret") labels := make(map[string]string) @@ -149,22 +154,26 @@ func (client *secretClient) createInProject() error { ProjectID: projectID, } - _, err = backendClient.Secret.Create(newSecret) - return err + if dryRun { + client.logger.WithField("object", newSecret).Info("Do Dry-Run Create") + } else { + _, err = backendClient.Secret.Create(newSecret) + } + return err == nil, err } -func (client *secretClient) createInNamespace() error { +func (client *secretClient) createInNamespace(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } projectID, err := client.project.ID() if err != nil { - return fmt.Errorf("Failed to read namespace ID, %v", err) + return changed, fmt.Errorf("Failed to read namespace ID, %v", err) } client.logger.Info("Create new Secret") labels := make(map[string]string) @@ -177,21 +186,25 @@ func (client *secretClient) createInNamespace() error { ProjectID: projectID, } - _, err = backendClient.NamespacedSecret.Create(newSecret) - return err + if dryRun { + client.logger.WithField("object", newSecret).Info("Do Dry-Run Create") + } else { + _, err = backendClient.NamespacedSecret.Create(newSecret) + } + return err == nil, err } -func (client *secretClient) Upgrade() error { +func (client *secretClient) Upgrade(dryRun bool) (changed bool, err error) { if client.namespace != "" { - return client.upgradeInNamespace() + return client.upgradeInNamespace(dryRun) } - return client.upgradeInProject() + return client.upgradeInProject(dryRun) } -func (client *secretClient) upgradeInProject() error { +func (client *secretClient) upgradeInProject(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } collection, err := backendClient.Secret.List(&types.ListOpts{ Filters: map[string]interface{}{ @@ -200,32 +213,36 @@ func (client *secretClient) upgradeInProject() error { }) if nil != err { client.logger.WithError(err).Error("Failed to read secret list") - return fmt.Errorf("Failed to read secret list, %v", err) + return changed, fmt.Errorf("Failed to read secret list, %v", err) } if len(collection.Data) == 0 { - return fmt.Errorf("Secret %v not found", client.name) + return changed, fmt.Errorf("Secret %v not found", client.name) } existingSecret := collection.Data[0] if isProjectSecretUnchanged(existingSecret, client.secret) { client.logger.Debug("Skip upgrade secret - no changes") - return nil + return } client.logger.Info("Upgrade Secret") existingSecret.Data = client.secret.Data - _, err = backendClient.Secret.Replace(&existingSecret) - return err + if dryRun { + client.logger.WithField("object", existingSecret).Info("Do Dry-Run Upgrade") + } else { + _, err = backendClient.Secret.Replace(&existingSecret) + } + return err == nil, err } -func (client *secretClient) upgradeInNamespace() error { +func (client *secretClient) upgradeInNamespace(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } collection, err := backendClient.NamespacedSecret.List(&types.ListOpts{ Filters: map[string]interface{}{ @@ -235,22 +252,26 @@ func (client *secretClient) upgradeInNamespace() error { }) if nil != err { client.logger.WithError(err).Error("Failed to read secret list") - return fmt.Errorf("Failed to read secret list, %v", err) + return changed, fmt.Errorf("Failed to read secret list, %v", err) } if len(collection.Data) == 0 { - return fmt.Errorf("Secret %v not found", client.name) + return changed, fmt.Errorf("Secret %v not found", client.name) } existingSecret := collection.Data[0] if isNamespacedSecretUnchanged(existingSecret, client.secret) { client.logger.Debug("Skip upgrade secret - no changes") - return nil + return } client.logger.Info("Upgrade Secret") existingSecret.Data = client.secret.Data - _, err = backendClient.NamespacedSecret.Replace(&existingSecret) - return err + if dryRun { + client.logger.WithField("object", existingSecret).Info("Do Dry-Run Upgrade") + } else { + _, err = backendClient.NamespacedSecret.Replace(&existingSecret) + } + return err == nil, err } func (client *secretClient) Data() (projectModel.ConfigMap, error) { diff --git a/internal/pkg/rancher/client/secret_client_test.go b/internal/pkg/rancher/client/secret_client_test.go index a91a4ff..7b609a9 100644 --- a/internal/pkg/rancher/client/secret_client_test.go +++ b/internal/pkg/rancher/client/secret_client_test.go @@ -99,7 +99,7 @@ func Test_secretClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/stateful_set_client.go b/internal/pkg/rancher/client/stateful_set_client.go index be4b35d..506cc5c 100644 --- a/internal/pkg/rancher/client/stateful_set_client.go +++ b/internal/pkg/rancher/client/stateful_set_client.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" "github.com/sirupsen/logrus" ) @@ -63,6 +64,10 @@ type statefulSetClient struct { statefulSet projectModel.StatefulSet } +func (client *statefulSetClient) Type() string { + return rancherModel.StatefulSetKind +} + func (client *statefulSetClient) Exists() (bool, error) { backendClient, err := client.project.backendProjectClient() if err != nil { @@ -91,28 +96,33 @@ func (client *statefulSetClient) Exists() (bool, error) { return false, nil } -func (client *statefulSetClient) Create() error { +func (client *statefulSetClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.project.backendProjectClient() if err != nil { - return err + return } client.logger.Info("Create new statefulSet") pattern, err := projectModel.ConvertStatefulSetToProjectAPI(client.statefulSet) if err != nil { - return err + return } namespaceID, err := client.NamespaceID() if err != nil { - return err + return } pattern.NamespaceId = namespaceID - _, err = backendClient.StatefulSet.Create(&pattern) - return err + + if dryRun { + client.logger.WithField("object", pattern).Info("Do Dry-Run Create") + } else { + _, err = backendClient.StatefulSet.Create(&pattern) + } + return err == nil, err } -func (client *statefulSetClient) Upgrade() error { +func (client *statefulSetClient) Upgrade(dryRun bool) (changed bool, err error) { client.logger.Warn("Skip change existing statefulset") - return nil + return } func (client *statefulSetClient) Data() (projectModel.StatefulSet, error) { diff --git a/internal/pkg/rancher/client/stateful_set_client_test.go b/internal/pkg/rancher/client/stateful_set_client_test.go index 1e3d604..dbf0ac0 100644 --- a/internal/pkg/rancher/client/stateful_set_client_test.go +++ b/internal/pkg/rancher/client/stateful_set_client_test.go @@ -99,7 +99,7 @@ func Test_statefulSetClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/client/storage_class_client.go b/internal/pkg/rancher/client/storage_class_client.go index f533abc..f222d12 100644 --- a/internal/pkg/rancher/client/storage_class_client.go +++ b/internal/pkg/rancher/client/storage_class_client.go @@ -18,6 +18,7 @@ import ( "fmt" projectModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/cluster/project/model" + rancherModel "github.com/bitgrip/cattlectl/internal/pkg/rancher/model" "github.com/rancher/norman/types" backendClusterClient "github.com/rancher/types/client/cluster/v3" "github.com/sirupsen/logrus" @@ -60,6 +61,10 @@ type storageClassClient struct { clusterClient ClusterClient } +func (client *storageClassClient) Type() string { + return rancherModel.StorageClass +} + func (client *storageClassClient) Exists() (bool, error) { backendClient, err := client.clusterClient.backendClusterClient() if err != nil { @@ -83,10 +88,10 @@ func (client *storageClassClient) Exists() (bool, error) { return false, nil } -func (client *storageClassClient) Create() error { +func (client *storageClassClient) Create(dryRun bool) (changed bool, err error) { backendClient, err := client.clusterClient.backendClusterClient() if err != nil { - return err + return } client.logger.Info("Create new storage class") newStorageClass := &backendClusterClient.StorageClass{ @@ -98,13 +103,17 @@ func (client *storageClassClient) Create() error { MountOptions: client.storageClass.MountOptions, } - _, err = backendClient.StorageClass.Create(newStorageClass) - return err + if dryRun { + client.logger.WithField("object", newStorageClass).Info("Do Dry-Run Create") + } else { + _, err = backendClient.StorageClass.Create(newStorageClass) + } + return err == nil, err } -func (client *storageClassClient) Upgrade() error { +func (client *storageClassClient) Upgrade(dryRun bool) (changed bool, err error) { client.logger.Debug("Skip change existing storage class") - return nil + return } func (client *storageClassClient) Data() (projectModel.StorageClass, error) { diff --git a/internal/pkg/rancher/client/storage_class_client_test.go b/internal/pkg/rancher/client/storage_class_client_test.go index ea6959c..6c46b3e 100644 --- a/internal/pkg/rancher/client/storage_class_client_test.go +++ b/internal/pkg/rancher/client/storage_class_client_test.go @@ -93,7 +93,7 @@ func Test_storageClassClient_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.client.Create() + _, err := tt.client.Create(false) if tt.wantErr { assert.NotOk(t, err, tt.wantedErr) } else { diff --git a/internal/pkg/rancher/descriptor/converger.go b/internal/pkg/rancher/descriptor/converger.go index b418b10..7656bcd 100644 --- a/internal/pkg/rancher/descriptor/converger.go +++ b/internal/pkg/rancher/descriptor/converger.go @@ -19,7 +19,17 @@ import ( ) type Converger interface { - Converge() error + Converge(bool) (ConvergeResult, error) +} + +type ConvergeResult struct { + CreatedResources []ResourceDescriptor `json:"created_resources"` + UpgradedResources []ResourceDescriptor `json:"upgraded_resources"` +} + +type ResourceDescriptor struct { + Type string + Name string } type ResourceClientConverger struct { @@ -27,28 +37,42 @@ type ResourceClientConverger struct { Children []Converger } -func (converger *ResourceClientConverger) Converge() error { +func (converger *ResourceClientConverger) Converge(dryRun bool) (result ConvergeResult, err error) { var ( - exists bool - err error + name string + exists bool + changed bool ) - exists, err = converger.Client.Exists() - if err != nil { - return err + if name, err = converger.Client.Name(); err != nil { + return + } + if exists, err = converger.Client.Exists(); err != nil { + return } if exists { - err = converger.Client.Upgrade() + changed, err = converger.Client.Upgrade(dryRun) + if err != nil { + return result, err + } + if changed { + result.UpgradedResources = append(result.UpgradedResources, ResourceDescriptor{Type: converger.Client.Type(), Name: name}) + } } else { - err = converger.Client.Create() - } - if err != nil { - return err + changed, err = converger.Client.Create(dryRun) + if err != nil { + return + } + if changed { + result.CreatedResources = append(result.CreatedResources, ResourceDescriptor{Type: converger.Client.Type(), Name: name}) + } } for _, child := range converger.Children { - err = child.Converge() + childResult, err := child.Converge(dryRun) + result.CreatedResources = append(result.CreatedResources, childResult.CreatedResources...) + result.UpgradedResources = append(result.UpgradedResources, childResult.UpgradedResources...) if err != nil { - return err + return result, err } } - return nil + return } diff --git a/internal/pkg/rancher/model/rancher.go b/internal/pkg/rancher/model/rancher.go index daaea47..9383ed8 100644 --- a/internal/pkg/rancher/model/rancher.go +++ b/internal/pkg/rancher/model/rancher.go @@ -16,14 +16,26 @@ package model // Types of Descriptors which are expected values of the field 'kind' const ( - RancherKind = "Rancher" - ClusterKind = "Cluster" - ProjectKind = "Project" - JobKind = "Job" - CronJobKind = "CronJob" - DeploymentKind = "Deployment" - DaemonSetKind = "DaemonSet" - StatefulSetKind = "StatefulSet" + RancherKind = "Rancher" + ClusterKind = "Cluster" + ProjectKind = "Project" + JobKind = "Job" + CronJobKind = "CronJob" + DeploymentKind = "Deployment" + DaemonSetKind = "DaemonSet" + StatefulSetKind = "StatefulSet" + App = "App" + Certificate = "Certificate" + ClusterCatalog = "ClusterCatalog" + Cluster = "Cluster" + ConfigMap = "ConfigMap" + DockerCredential = "DockerCredential" + Namespace = "Namespace" + PersistentVolume = "PersistentVolume" + ProjectCatalog = "ProjectCatalog" + RancherCatalog = "RancherCatalog" + Secret = "Secret" + StorageClass = "StorageClass" ) // Rancher represents global members diff --git a/vendor/modules.txt b/vendor/modules.txt index 36978ef..7b2618c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -10,7 +10,6 @@ github.com/ghodss/yaml # github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d github.com/gogo/protobuf/proto # github.com/google/gofuzz v1.0.0 -## explicit github.com/google/gofuzz # github.com/gorilla/websocket v1.4.0 github.com/gorilla/websocket @@ -31,7 +30,6 @@ github.com/inconshreveable/mousetrap ## explicit github.com/konsorten/go-windows-terminal-sequences # github.com/magiconair/properties v1.8.1 -## explicit github.com/magiconair/properties # github.com/mitchellh/go-homedir v1.1.0 ## explicit @@ -42,7 +40,6 @@ github.com/mitchellh/mapstructure ## explicit github.com/pelletier/go-toml # github.com/pkg/errors v0.8.1 -## explicit github.com/pkg/errors # github.com/rancher/norman v0.0.0-20200227003532-35fa47cccad7 ## explicit @@ -79,7 +76,6 @@ github.com/spf13/cast github.com/spf13/cobra github.com/spf13/cobra/doc # github.com/spf13/jwalterweatherman v1.1.0 -## explicit github.com/spf13/jwalterweatherman # github.com/spf13/pflag v1.0.3 github.com/spf13/pflag