From 890b5c5202e3f7a97e5bf516b1b14877fc17a26f Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Mon, 15 Feb 2021 16:34:55 +0100 Subject: [PATCH 1/5] Use label selector to check components Signed-off-by: Hidde Beydals --- cmd/flux/check.go | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/cmd/flux/check.go b/cmd/flux/check.go index 3404c396..fad6a6cb 100644 --- a/cmd/flux/check.go +++ b/cmd/flux/check.go @@ -21,14 +21,16 @@ import ( "encoding/json" "os" "os/exec" - "strings" "time" "github.com/blang/semver/v4" - "github.com/fluxcd/flux2/internal/utils" "github.com/spf13/cobra" + v1 "k8s.io/api/apps/v1" apimachineryversion "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/fluxcd/flux2/internal/utils" ) var checkCmd = &cobra.Command{ @@ -176,23 +178,29 @@ func componentsCheck() bool { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - statusChecker, err := NewStatusChecker(time.Second, 30*time.Second) + kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) if err != nil { return false } - ok := true - deployments := append(checkArgs.components, checkArgs.extraComponents...) - for _, deployment := range deployments { - if err := statusChecker.Assess(deployment); err != nil { - ok = false - } else { - logger.Successf("%s: healthy", deployment) - } + statusChecker, err := NewStatusChecker(time.Second, rootArgs.timeout) + if err != nil { + return false + } - kubectlArgs := []string{"-n", rootArgs.namespace, "get", "deployment", deployment, "-o", "jsonpath=\"{..image}\""} - if output, err := utils.ExecKubectlCommand(ctx, utils.ModeCapture, rootArgs.kubeconfig, rootArgs.kubecontext, kubectlArgs...); err == nil { - logger.Actionf(strings.TrimPrefix(strings.TrimSuffix(output, "\""), "\"")) + ok := true + selector := client.MatchingLabels{"app.kubernetes.io/instance": rootArgs.namespace} + var list v1.DeploymentList + if err := kubeClient.List(ctx, &list, client.InNamespace(rootArgs.namespace), selector); err == nil { + for _, d := range list.Items { + if err := statusChecker.Assess(d.Name); err != nil { + ok = false + } else { + logger.Successf("%s: healthy", d.Name) + } + for _, c := range d.Spec.Template.Spec.Containers { + logger.Actionf(c.Image) + } } } return ok From b903cd5b68e98d3106e806f3dc5c5e644a3d6944 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Mon, 15 Feb 2021 17:05:01 +0100 Subject: [PATCH 2/5] Check for new Flux binary version This prints a warning if the user has internet access and is running an older version of the binary. It also replaces the `blang/semver` package with `pkg/version` and `Masterminds/semver` to align with controller dependencies. Signed-off-by: Hidde Beydals --- cmd/flux/check.go | 59 ++++++++++++++++++++++++++++++++++------------- go.mod | 3 ++- go.sum | 7 +++--- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/cmd/flux/check.go b/cmd/flux/check.go index fad6a6cb..83fec660 100644 --- a/cmd/flux/check.go +++ b/cmd/flux/check.go @@ -23,14 +23,17 @@ import ( "os/exec" "time" - "github.com/blang/semver/v4" + "github.com/Masterminds/semver/v3" "github.com/spf13/cobra" v1 "k8s.io/api/apps/v1" apimachineryversion "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/fluxcd/pkg/version" + "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/pkg/manifestgen/install" ) var checkCmd = &cobra.Command{ @@ -76,6 +79,8 @@ func runCheckCmd(cmd *cobra.Command, args []string) error { logger.Actionf("checking prerequisites") checkFailed := false + fluxCheck() + if !kubectlCheck(ctx, ">=1.18.0") { checkFailed = true } @@ -103,7 +108,29 @@ func runCheckCmd(cmd *cobra.Command, args []string) error { return nil } -func kubectlCheck(ctx context.Context, version string) bool { +func fluxCheck() { + curSv, err := version.ParseVersion(VERSION) + if err != nil { + return + } + // Exclude development builds. + if curSv.Prerelease() != "" { + return + } + latest, err := install.GetLatestVersion() + if err != nil { + return + } + latestSv, err := version.ParseVersion(latest) + if err != nil { + return + } + if latestSv.GreaterThan(curSv) { + logger.Failuref("flux %s <%s (new version is available, please upgrade)", curSv, latestSv) + } +} + +func kubectlCheck(ctx context.Context, constraint string) bool { _, err := exec.LookPath("kubectl") if err != nil { logger.Failuref("kubectl not found") @@ -119,58 +146,58 @@ func kubectlCheck(ctx context.Context, version string) bool { kv := &kubectlVersion{} if err = json.Unmarshal([]byte(output), kv); err != nil { - logger.Failuref("kubectl version output can't be unmarshaled") + logger.Failuref("kubectl version output can't be unmarshalled") return false } - v, err := semver.ParseTolerant(kv.ClientVersion.GitVersion) + v, err := version.ParseVersion(kv.ClientVersion.GitVersion) if err != nil { logger.Failuref("kubectl version can't be parsed") return false } - rng, _ := semver.ParseRange(version) - if !rng(v) { - logger.Failuref("kubectl version must be %s", version) + c, _ := semver.NewConstraint(constraint) + if !c.Check(v) { + logger.Failuref("kubectl version must be %s", constraint) return false } - logger.Successf("kubectl %s %s", v.String(), version) + logger.Successf("kubectl %s %s", v.String(), constraint) return true } -func kubernetesCheck(version string) bool { +func kubernetesCheck(constraint string) bool { cfg, err := utils.KubeConfig(rootArgs.kubeconfig, rootArgs.kubecontext) if err != nil { logger.Failuref("Kubernetes client initialization failed: %s", err.Error()) return false } - client, err := kubernetes.NewForConfig(cfg) + clientSet, err := kubernetes.NewForConfig(cfg) if err != nil { logger.Failuref("Kubernetes client initialization failed: %s", err.Error()) return false } - ver, err := client.Discovery().ServerVersion() + kv, err := clientSet.Discovery().ServerVersion() if err != nil { logger.Failuref("Kubernetes API call failed: %s", err.Error()) return false } - v, err := semver.ParseTolerant(ver.String()) + v, err := version.ParseVersion(kv.String()) if err != nil { logger.Failuref("Kubernetes version can't be determined") return false } - rng, _ := semver.ParseRange(version) - if !rng(v) { - logger.Failuref("Kubernetes version must be %s", version) + c, _ := semver.NewConstraint(constraint) + if !c.Check(v) { + logger.Failuref("Kubernetes version must be %s", constraint) return false } - logger.Successf("Kubernetes %s %s", v.String(), version) + logger.Successf("Kubernetes %s %s", v.String(), constraint) return true } diff --git a/go.mod b/go.mod index 0d69186b..7f4b6bb4 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/fluxcd/flux2 go 1.15 require ( - github.com/blang/semver/v4 v4.0.0 + github.com/Masterminds/semver/v3 v3.1.0 github.com/cyphar/filepath-securejoin v0.2.2 github.com/fluxcd/helm-controller/api v0.7.0 github.com/fluxcd/image-automation-controller/api v0.5.0 @@ -15,6 +15,7 @@ require ( github.com/fluxcd/pkg/runtime v0.8.0 github.com/fluxcd/pkg/ssh v0.0.5 github.com/fluxcd/pkg/untar v0.0.5 + github.com/fluxcd/pkg/version v0.0.1 github.com/fluxcd/source-controller/api v0.8.0 github.com/google/go-containerregistry v0.2.0 github.com/manifoldco/promptui v0.7.0 diff --git a/go.sum b/go.sum index 5c6684f8..9c3c39b6 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= +github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -112,10 +114,7 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -220,6 +219,8 @@ github.com/fluxcd/pkg/ssh v0.0.5 h1:rnbFZ7voy2JBlUfMbfyqArX2FYaLNpDhccGFC3qW83A= github.com/fluxcd/pkg/ssh v0.0.5/go.mod h1:7jXPdXZpc0ttMNz2kD9QuMi3RNn/e0DOFbj0Tij/+Hs= github.com/fluxcd/pkg/untar v0.0.5 h1:UGI3Ch1UIEIaqQvMicmImL1s9npQa64DJ/ozqHKB7gk= github.com/fluxcd/pkg/untar v0.0.5/go.mod h1:O6V9+rtl8c1mHBafgqFlJN6zkF1HS5SSYn7RpQJ/nfw= +github.com/fluxcd/pkg/version v0.0.1 h1:/8asQoDXSThz3csiwi4Qo8Zb6blAxLXbtxNgeMJ9bCg= +github.com/fluxcd/pkg/version v0.0.1/go.mod h1:WAF4FEEA9xyhngF8TDxg3UPu5fA1qhEYV8Pmi2Il01Q= github.com/fluxcd/source-controller/api v0.8.0 h1:jOgeOwCLXzmjinRiDT7e/IuSB7WNZMgrUwMLJm09K/o= github.com/fluxcd/source-controller/api v0.8.0/go.mod h1:u2sdc/QDm0tzXHL7mZVj928hc3MMU+4mKCuAQg+94Bk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= From 9b649f6c72c6211c0dd120bf424fb213a3bb107b Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Mon, 15 Feb 2021 18:41:25 +0100 Subject: [PATCH 3/5] Check if targeted bootstrap/install version exists Signed-off-by: Hidde Beydals --- cmd/flux/bootstrap.go | 7 +++++++ cmd/flux/install.go | 31 ++++++++++++++++++------------ pkg/manifestgen/install/install.go | 31 +++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/cmd/flux/bootstrap.go b/cmd/flux/bootstrap.go index d48ed437..c87ccf43 100644 --- a/cmd/flux/bootstrap.go +++ b/cmd/flux/bootstrap.go @@ -132,6 +132,13 @@ func generateInstallManifests(targetPath, namespace, tmpDir string, localManifes return "", err } bootstrapArgs.version = version + } else { + if ok, err := install.ExistingVersion(bootstrapArgs.version); err != nil || !ok { + if err == nil { + err = fmt.Errorf("targeted version '%s' does not exist", bootstrapArgs.version) + } + return "", err + } } opts := install.Options{ diff --git a/cmd/flux/install.go b/cmd/flux/install.go index 8bd02327..7816023f 100644 --- a/cmd/flux/install.go +++ b/cmd/flux/install.go @@ -106,19 +106,9 @@ func installCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - tmpDir, err := ioutil.TempDir("", rootArgs.namespace) - if err != nil { - return err - } - defer os.RemoveAll(tmpDir) - - if !installExport { - logger.Generatef("generating manifests") - } - components := append(installDefaultComponents, installExtraComponents...) - - if err := utils.ValidateComponents(components); err != nil { + err := utils.ValidateComponents(components) + if err != nil { return err } @@ -127,6 +117,23 @@ func installCmdRun(cmd *cobra.Command, args []string) error { if err != nil { return err } + } else { + if ok, err := install.ExistingVersion(installVersion); err != nil || !ok { + if err == nil { + err = fmt.Errorf("targeted version '%s' does not exist", installVersion) + } + return err + } + } + + tmpDir, err := ioutil.TempDir("", rootArgs.namespace) + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + if !installExport { + logger.Generatef("generating manifests") } opts := install.Options{ diff --git a/pkg/manifestgen/install/install.go b/pkg/manifestgen/install/install.go index feb83046..f5168545 100644 --- a/pkg/manifestgen/install/install.go +++ b/pkg/manifestgen/install/install.go @@ -87,7 +87,7 @@ func GetLatestVersion() (string, error) { res, err := c.Get(ghURL) if err != nil { - return "", fmt.Errorf("calling GitHub API failed: %w", err) + return "", fmt.Errorf("GitHub API call failed: %w", err) } if res.Body != nil { @@ -104,3 +104,32 @@ func GetLatestVersion() (string, error) { return m.Tag, err } + +// ExistingVersion calls the GitHub API to confirm the given version does exist. +func ExistingVersion(version string) (bool, error) { + if !strings.HasPrefix(version, "v") { + version = "v" + version + } + + ghURL := fmt.Sprintf("https://api.github.com/repos/fluxcd/flux2/releases/tags/%s", version) + c := http.DefaultClient + c.Timeout = 15 * time.Second + + res, err := c.Get(ghURL) + if err != nil { + return false, fmt.Errorf("GitHub API call failed: %w", err) + } + + if res.Body != nil { + defer res.Body.Close() + } + + switch res.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusNotFound: + return false, nil + default: + return false, fmt.Errorf("GitHub API returned an unexpected status code (%d)", res.StatusCode) + } +} From 5263dabd222def8b1527926e6a65df11dd7ab6f6 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Mon, 15 Feb 2021 20:50:31 +0100 Subject: [PATCH 4/5] Check if targeted version is supported by binary Signed-off-by: Hidde Beydals --- cmd/flux/bootstrap.go | 4 ++++ cmd/flux/install.go | 4 ++++ internal/utils/utils.go | 24 ++++++++++++++++++++- internal/utils/utils_test.go | 42 ++++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 internal/utils/utils_test.go diff --git a/cmd/flux/bootstrap.go b/cmd/flux/bootstrap.go index c87ccf43..b90e7160 100644 --- a/cmd/flux/bootstrap.go +++ b/cmd/flux/bootstrap.go @@ -141,6 +141,10 @@ func generateInstallManifests(targetPath, namespace, tmpDir string, localManifes } } + if !utils.CompatibleVersion(VERSION, bootstrapArgs.version) { + return "", fmt.Errorf("targeted version '%s' is not compatible with your current version of flux (%s)", bootstrapArgs.version, VERSION) + } + opts := install.Options{ BaseURL: localManifests, Version: bootstrapArgs.version, diff --git a/cmd/flux/install.go b/cmd/flux/install.go index 7816023f..bac6d058 100644 --- a/cmd/flux/install.go +++ b/cmd/flux/install.go @@ -126,6 +126,10 @@ func installCmdRun(cmd *cobra.Command, args []string) error { } } + if !utils.CompatibleVersion(VERSION, installVersion) { + return fmt.Errorf("targeted version '%s' is not compatible with your current version of flux (%s)", installVersion, VERSION) + } + tmpDir, err := ioutil.TempDir("", rootArgs.namespace) if err != nil { return err diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 5e8f20d5..358bdf0e 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -46,14 +46,16 @@ import ( kustypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/yaml" - "github.com/fluxcd/flux2/pkg/manifestgen/install" helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" imageautov1 "github.com/fluxcd/image-automation-controller/api/v1alpha1" imagereflectv1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" "github.com/fluxcd/pkg/runtime/dependency" + "github.com/fluxcd/pkg/version" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + + "github.com/fluxcd/flux2/pkg/manifestgen/install" ) type Utils struct { @@ -420,3 +422,23 @@ func MergeMaps(a, b map[string]interface{}) map[string]interface{} { } return out } + +// CompatibleVersion returns if the provided binary version is compatible +// with the given target version. At present, this is true if the target +// version is equal to the MINOR range of the binary, or if the binary +// version is a prerelease. +func CompatibleVersion(binary, target string) bool { + binSv, err := version.ParseVersion(binary) + if err != nil { + return false + } + // Assume prerelease builds are compatible. + if binSv.Prerelease() != "" { + return true + } + targetSv, err := version.ParseVersion(target) + if err != nil { + return false + } + return binSv.Major() == targetSv.Major() && binSv.Minor() == targetSv.Minor() +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 00000000..b4ed6eb4 --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,42 @@ +/* +Copyright 2021 The Flux authors + +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 "testing" + +func TestCompatibleVersion(t *testing.T) { + tests := []struct { + name string + binary string + target string + want bool + }{ + {"different major version", "1.1.0", "0.1.0", false}, + {"different minor version", "0.1.0", "0.2.0", false}, + {"same version", "0.1.0", "0.1.0", true}, + {"binary patch version ahead", "0.1.1", "0.1.0", true}, + {"target patch version ahead", "0.1.1", "0.1.2", true}, + {"prerelease binary", "0.0.0-dev.0", "0.1.0", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CompatibleVersion(tt.binary, tt.target); got != tt.want { + t.Errorf("CompatibleVersion() = %v, want %v", got, tt.want) + } + }) + } +} From 5a21f502309c7bbdeb5d0b062b3bfb2f0a92b5ea Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Mon, 15 Feb 2021 20:51:27 +0100 Subject: [PATCH 5/5] Remove unused util functions Signed-off-by: Hidde Beydals --- internal/utils/utils.go | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 358bdf0e..94b26866 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -205,41 +205,6 @@ func SplitKubeConfigPath(path string) []string { return strings.Split(path, sep) } -func WriteFile(content, filename string) error { - file, err := os.Create(filename) - if err != nil { - return err - } - defer file.Close() - - _, err = io.WriteString(file, content) - if err != nil { - return err - } - - return file.Sync() -} - -func CopyFile(src, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - - out, err := os.Create(dst) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, in) - if err != nil { - return err - } - return out.Close() -} - func ContainsItemString(s []string, e string) bool { for _, a := range s { if a == e {