diff --git a/app/cni/cmd/install/main.go b/app/cni/cmd/install/main.go index cd8b54128344..c1d82855105c 100644 --- a/app/cni/cmd/install/main.go +++ b/app/cni/cmd/install/main.go @@ -1,9 +1,11 @@ package main import ( + "context" + "github.com/kumahq/kuma/app/cni/pkg/install" ) func main() { - install.Run() + install.Run(context.Background()) } diff --git a/app/cni/pkg/install/installer_config.go b/app/cni/pkg/install/installer_config.go index d90c9796888d..7af185ce83b1 100644 --- a/app/cni/pkg/install/installer_config.go +++ b/app/cni/pkg/install/installer_config.go @@ -1,7 +1,9 @@ package install import ( + "context" "encoding/base64" + "fmt" "net" "net/url" "os" @@ -12,34 +14,36 @@ import ( "github.com/pkg/errors" "github.com/kumahq/kuma/pkg/config" + "github.com/kumahq/kuma/pkg/util/files" ) const ( defaultKumaCniConfName = "YYY-kuma-cni.conflist" ) -var _ config.Config = InstallerConfig{} +var _ config.Config = &InstallerConfig{} type InstallerConfig struct { config.BaseConfig - CfgCheckInterval int `envconfig:"cfgcheck_interval" default:"1"` - ChainedCniPlugin bool `envconfig:"chained_cni_plugin" default:"true"` - CniConfName string `envconfig:"cni_conf_name" default:""` - CniLogLevel string `envconfig:"cni_log_level" default:"info"` - CniNetworkConfig string `envconfig:"cni_network_config" default:""` - HostCniNetDir string `envconfig:"cni_net_dir" default:"/etc/cni/net.d"` - KubeconfigName string `envconfig:"kubecfg_file_name" default:"ZZZ-kuma-cni-kubeconfig"` - KubernetesCaFile string `envconfig:"kube_ca_file"` - KubernetesServiceHost string `envconfig:"kubernetes_service_host"` - KubernetesServicePort string `envconfig:"kubernetes_service_port"` - KubernetesServiceProtocol string `envconfig:"kubernetes_service_protocol" default:"https"` - MountedCniNetDir string `envconfig:"mounted_cni_net_dir" default:"/host/etc/cni/net.d"` - ShouldSleep bool `envconfig:"sleep" default:"true"` + CfgCheckInterval int `envconfig:"cfgcheck_interval" default:"1"` + ChainedCniPlugin bool `envconfig:"chained_cni_plugin" default:"true"` + CniConfName string `envconfig:"cni_conf_name" default:""` + CniLogLevel string `envconfig:"cni_log_level" default:"info"` + CniNetworkConfig string `envconfig:"cni_network_config" default:""` + HostCniNetDir string `envconfig:"cni_net_dir" default:"/etc/cni/net.d"` + KubeconfigName string `envconfig:"kubecfg_file_name" default:"ZZZ-kuma-cni-kubeconfig"` + KubernetesCaFile string `envconfig:"kube_ca_file" default:"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"` + KubernetesServiceAccountTokenPath string `envconfig:"kube_service_account_token_path" default:"/var/run/secrets/kubernetes.io/serviceaccount/token"` + KubernetesServiceHost string `envconfig:"kubernetes_service_host"` + KubernetesServicePort string `envconfig:"kubernetes_service_port"` + KubernetesServiceProtocol string `envconfig:"kubernetes_service_protocol" default:"https"` + MountedCniNetDir string `envconfig:"mounted_cni_net_dir" default:"/host/etc/cni/net.d"` + ShouldSleep bool `envconfig:"sleep" default:"true"` } -func (i InstallerConfig) Validate() error { - if i.CfgCheckInterval <= 0 { +func (c *InstallerConfig) Validate() error { + if c.CfgCheckInterval <= 0 { return errors.New("CFGCHECK_INTERVAL env variable needs to be greater than 0") } @@ -48,115 +52,169 @@ func (i InstallerConfig) Validate() error { return nil } -func findCniConfFile(mountedCNINetDir string) (string, error) { - matches, err := filepath.Glob(mountedCNINetDir + "/*.conf") - if err != nil { - return "", err +func (c *InstallerConfig) PostProcess() error { + if c.CniConfName != "" { + return nil } - file, found := lookForValidConfig(matches, isValidConfFile) - if found { - return filepath.Base(file), nil - } + for _, ext := range []string{"*.conf", "*.conflist"} { + matches, err := filepath.Glob(filepath.Join(c.MountedCniNetDir, ext)) + if err != nil { + log.Info("failed to search for CNI config files", "error", err) + continue + } - matches, err = filepath.Glob(mountedCNINetDir + "/*.conflist") - if err != nil { - return "", err - } - file, found = lookForValidConfig(matches, isValidConflistFile) - if found { - return filepath.Base(file), nil + if file, ok := lookForValidConfig(matches, isValidConfFile); ok { + log.Info("found CNI config file", "file", file) + c.CniConfName = filepath.Base(file) + return nil + } } - // use default - return "", errors.New("cni conf file not found - use default") + log.Info("could not find CNI config file, using default") + c.CniConfName = defaultKumaCniConfName + + return nil } -func prepareKubeconfig(ic *InstallerConfig, serviceAccountPath string) error { - kubeconfigPath := ic.MountedCniNetDir + "/" + ic.KubeconfigName - serviceAccountTokenPath := serviceAccountPath + "/token" - serviceAccountToken, err := os.ReadFile(serviceAccountTokenPath) +func (c *InstallerConfig) PrepareKubeconfig() error { + token, err := os.ReadFile(c.KubernetesServiceAccountTokenPath) if err != nil { - return err - } - - if ic.KubernetesServiceHost == "" { - return errors.New("KUBERNETES_SERVICE_HOST env variable not set") + return errors.Wrap(err, "failed to read service account token") } - if ic.KubernetesServicePort == "" { - return errors.New("KUBERNETES_SERVICE_PORT env variable not set") + if c.KubernetesServiceHost == "" { + return errors.New("kubernetes service host is not set") } - if ic.KubernetesCaFile == "" { - ic.KubernetesCaFile = serviceAccountPath + "/ca.crt" + if c.KubernetesServicePort == "" { + return errors.New("kubernetes service port is not set") } - kubeCa, err := os.ReadFile(ic.KubernetesCaFile) + kubeCa, err := os.ReadFile(c.KubernetesCaFile) if err != nil { - return err + return errors.Wrap(err, "failed to read Kubernetes CA file") } - caData := base64.StdEncoding.EncodeToString(kubeCa) - kubeconfig := kubeconfigTemplate(ic.KubernetesServiceProtocol, ic.KubernetesServiceHost, ic.KubernetesServicePort, string(serviceAccountToken), caData) - log.Info("writing kubernetes config", "path", kubeconfigPath) - err = atomic.WriteFile(kubeconfigPath, strings.NewReader(kubeconfig)) - if err != nil { - return err + return c.writeKubeconfig(c.GenerateKubeconfigTemplate(token, kubeCa)) +} + +func (c *InstallerConfig) writeKubeconfig(kubeconfig string) error { + path := filepath.Join(c.MountedCniNetDir, c.KubeconfigName) + + log.Info("writing kubernetes config", "path", path) + + if err := atomic.WriteFile(path, strings.NewReader(kubeconfig)); err != nil { + return errors.Wrap(err, "failed to write kubeconfig") } return nil } -func kubeconfigTemplate(protocol, host, port, token, caData string) string { - serverUrl := url.URL{ - Scheme: protocol, - Host: net.JoinHostPort(host, port), - } - - return `# Kubeconfig file for kuma CNI plugin. +func (c *InstallerConfig) GenerateKubeconfigTemplate(token, caData []byte) string { + return fmt.Sprintf( + `# Kubeconfig file for kuma CNI plugin. apiVersion: v1 kind: Config clusters: - name: local cluster: - server: ` + serverUrl.String() + ` - certificate-authority-data: ` + caData + ` + server: %s + certificate-authority-data: %s users: - name: kuma-cni user: - token: ` + token + ` + token: %s contexts: - name: kuma-cni-context context: cluster: local user: kuma-cni -current-context: kuma-cni-context` +current-context: kuma-cni-context`, + c.kubernetesServiceURL(), + base64.StdEncoding.EncodeToString(caData), + token, + ) +} + +func (c *InstallerConfig) kubernetesServiceURL() *url.URL { + return &url.URL{ + Scheme: c.KubernetesServiceProtocol, + Host: net.JoinHostPort(c.KubernetesServiceHost, c.KubernetesServicePort), + } +} + +func (c *InstallerConfig) PrepareKumaCniConfig(ctx context.Context) error { + token, err := os.ReadFile(c.KubernetesServiceAccountTokenPath) + if err != nil { + return errors.Wrap(err, "failed to read service account token") + } + + // Replace placeholders in the CNI network configuration + cniConfig := strings.NewReplacer( + "__KUBECONFIG_FILEPATH__", filepath.Join(c.HostCniNetDir, c.KubeconfigName), + "__SERVICEACCOUNT_TOKEN__", string(token), + ).Replace(c.CniNetworkConfig) + + log.V(1).Info("CNI config after replacement", "CNI config", cniConfig) + + if c.ChainedCniPlugin { + if err := setupChainedPlugin(ctx, c.MountedCniNetDir, c.CniConfName, cniConfig); err != nil { + return errors.Wrap(err, "unable to setup kuma CNI as chained plugin") + } + + return nil + } + + configPath := filepath.Join(c.MountedCniNetDir, c.CniConfName) + log.Info("writing standalone CNI config", "path", configPath) + + if err := atomic.WriteFile(configPath, strings.NewReader(cniConfig)); err != nil { + return errors.Wrap(err, "failed to write standalone CNI config") + } + + return nil } -func prepareKumaCniConfig(ic *InstallerConfig, serviceAccountPath string) error { - rawConfig := ic.CniNetworkConfig - kubeconfigFilePath := ic.HostCniNetDir + "/" + ic.KubeconfigName +func (c *InstallerConfig) CheckInstall() error { + confPath := filepath.Join(c.MountedCniNetDir, c.CniConfName) - cniConfig := strings.Replace(rawConfig, "__KUBECONFIG_FILEPATH__", kubeconfigFilePath, 1) - log.V(1).Info("cni config after replace", "cni config", cniConfig) + if !files.FileExists(confPath) { + return errors.Errorf("cni config file does not exist at the specified path: %s", confPath) + } - serviceAccountToken, err := os.ReadFile(serviceAccountPath + "/token") + parsed, err := parseFileToHashMap(confPath) if err != nil { - return err + return errors.Wrap(err, "failed to parse cni config file") } - cniConfig = strings.Replace(cniConfig, "__SERVICEACCOUNT_TOKEN__", string(serviceAccountToken), 1) - if ic.ChainedCniPlugin { - err := setupChainedPlugin(ic.MountedCniNetDir, ic.CniConfName, cniConfig) - if err != nil { - return errors.Wrap(err, "unable to setup kuma cni as chained plugin") + if c.ChainedCniPlugin { + if err := isValidConflistFile(confPath); err != nil { + return errors.Wrap(err, "chained plugin requires a valid conflist file format") } - } else { - err := atomic.WriteFile(ic.MountedCniNetDir+"/"+ic.CniConfName, strings.NewReader(cniConfig)) + + plugins, err := getPluginsArray(parsed) if err != nil { - return err + return errors.Wrap(err, "failed to retrieve plugins array from cni config") } + + if index, err := findKumaCniConfigIndex(plugins); err != nil { + return errors.Wrap(err, "failed to find kuma-cni plugin in chained config file") + } else if index < 0 { + return errors.New("kuma-cni plugin is missing in the chained config file") + } + + return nil + } + + if err := isValidConfFile(confPath); err != nil { + return errors.Wrap(err, "standalone plugin requires a valid conf file format") + } + + if pluginType, ok := parsed["type"]; !ok { + return errors.New("cni config is missing the required 'type' field") + } else if pluginType != "kuma-cni" { + return errors.New("cni config 'type' field is not set to 'kuma-cni'") } return nil @@ -164,20 +222,9 @@ func prepareKumaCniConfig(ic *InstallerConfig, serviceAccountPath string) error func loadInstallerConfig() (*InstallerConfig, error) { var installerConfig InstallerConfig - err := config.Load("", &installerConfig) - if err != nil { - return nil, err - } - if installerConfig.CniConfName == "" { - cniConfFile, err := findCniConfFile(installerConfig.MountedCniNetDir) - if err != nil { - log.Info("could not find cni conf file using default") - installerConfig.CniConfName = defaultKumaCniConfName - } else { - log.Info("found CNI config file", "file", cniConfFile) - installerConfig.CniConfName = cniConfFile - } + if err := config.Load("", &installerConfig); err != nil { + return nil, err } return &installerConfig, nil diff --git a/app/cni/pkg/install/installer_config_test.go b/app/cni/pkg/install/installer_config_test.go index 35896c8db6c8..7d396ab25a12 100644 --- a/app/cni/pkg/install/installer_config_test.go +++ b/app/cni/pkg/install/installer_config_test.go @@ -1,8 +1,10 @@ package install import ( + "context" "os" "path" + "path/filepath" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -10,104 +12,190 @@ import ( "github.com/kumahq/kuma/pkg/test/matchers" ) -var _ = Describe("findCniConfFile", func() { - It("should find conf file in a flat dir", func() { - // given - dir := path.Join("testdata", "find-conf-dir") - - // when - result, err := findCniConfFile(dir) - - Expect(err).To(Not(HaveOccurred())) - Expect(result).To(Equal("10-flannel.conf")) - }) - - It("should find conflist file in a dir", func() { - // given - dir := path.Join("testdata", "find-conflist-dir") - - // when - result, err := findCniConfFile(dir) - - Expect(err).To(Not(HaveOccurred())) - Expect(result).To(Equal("10-calico.conflist")) +const expectedKubeconfig = `# Kubeconfig file for kuma CNI plugin. +apiVersion: v1 +kind: Config +clusters: +- name: local + cluster: + server: https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:3000 + certificate-authority-data: YWJjCg== +users: +- name: kuma-cni + user: + token: token +contexts: +- name: kuma-cni-context + context: + cluster: local + user: kuma-cni +current-context: kuma-cni-context` + +var _ = Describe("InstallerConfig", func() { + Describe("PostProcess", func() { + It("should use default CNI config when none is found", func() { + // given + ic := InstallerConfig{ + MountedCniNetDir: path.Join("testdata", "nonexistent-dir"), + } + + // when + err := ic.PostProcess() + + // then + Expect(err).To(Not(HaveOccurred())) + Expect(ic.CniConfName).To(Equal(defaultKumaCniConfName)) + }) + + It("should find and use the CNI config file if it exists", func() { + // given + ic := InstallerConfig{ + MountedCniNetDir: path.Join("testdata", "find-conf-dir"), + } + + // when + err := ic.PostProcess() + + // then + Expect(err).To(Not(HaveOccurred())) + Expect(ic.CniConfName).To(Equal("10-flannel.conf")) + }) }) - It("should not find conf file in a nested dir", func() { - // given - dir := path.Join("testdata", "find-conf-dir-nested") - - // when - result, err := findCniConfFile(dir) - - Expect(err).To(HaveOccurred()) - Expect(result).To(Equal("")) + Describe("PrepareKubeconfig", func() { + It("should successfully prepare kubeconfig file", func() { + // given + mockServiceAccountPath := filepath.Join("testdata", "prepare-kubeconfig") + ic := InstallerConfig{ + KubernetesServiceHost: "localhost", + KubernetesServicePort: "3000", + KubernetesServiceProtocol: "https", + KubernetesCaFile: filepath.Join(mockServiceAccountPath, "ca.crt"), + KubernetesServiceAccountTokenPath: filepath.Join(mockServiceAccountPath, "token"), + MountedCniNetDir: filepath.Join("testdata", "prepare-kubeconfig"), + KubeconfigName: "ZZZ-kuma-cni-kubeconfig", + } + + // when + err := ic.PrepareKubeconfig() + + // then + Expect(err).To(Not(HaveOccurred())) + // and + kubeconfig, _ := os.ReadFile(filepath.Join("testdata", "prepare-kubeconfig", "ZZZ-kuma-cni-kubeconfig")) + Expect(kubeconfig).To(matchers.MatchGoldenYAML(filepath.Join("testdata", "prepare-kubeconfig", "ZZZ-kuma-cni-kubeconfig.golden"))) + }) }) -}) -var _ = Describe("prepareKubeconfig", func() { - It("should successfully prepare kubeconfig file", func() { - // given - mockServiceAccountPath := path.Join("testdata", "prepare-kubeconfig") - ic := InstallerConfig{ - KubernetesServiceHost: "localhost", - KubernetesServicePort: "3000", - KubernetesServiceProtocol: "https", - MountedCniNetDir: path.Join("testdata", "prepare-kubeconfig"), - KubeconfigName: "ZZZ-kuma-cni-kubeconfig", - } - - // when - err := prepareKubeconfig(&ic, mockServiceAccountPath) - - // then - Expect(err).To(Not(HaveOccurred())) - // and - kubeconfig, _ := os.ReadFile(path.Join("testdata", "prepare-kubeconfig", "ZZZ-kuma-cni-kubeconfig")) - Expect(kubeconfig).To(matchers.MatchGoldenYAML(path.Join("testdata", "prepare-kubeconfig", "ZZZ-kuma-cni-kubeconfig.golden"))) + Describe("GenerateKubeconfigTemplate", func() { + It("should work properly with unescaped IPv6 addresses", func() { + // given + ic := InstallerConfig{ + KubernetesServiceProtocol: "https", + KubernetesServiceHost: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + KubernetesServicePort: "3000", + } + + // when + result := ic.GenerateKubeconfigTemplate([]byte("token"), []byte("abc\n")) + + // then + Expect(result).To(Equal(expectedKubeconfig)) + }) }) -}) -var _ = Describe("prepareKumaCniConfig", func() { - It("should successfully prepare chained kuma cni file", func() { - // given - mockServiceAccountPath := path.Join("testdata", "prepare-chained-kuma-config") - ic := InstallerConfig{ - CniNetworkConfig: kumaCniConfigTemplate, - MountedCniNetDir: path.Join("testdata", "prepare-chained-kuma-config"), - KubeconfigName: "ZZZ-kuma-cni-kubeconfig", - CniConfName: "10-calico.conflist", - ChainedCniPlugin: true, - } - - // when - err := prepareKumaCniConfig(&ic, mockServiceAccountPath) - - // then - Expect(err).To(Not(HaveOccurred())) - // and - kubeconfig, _ := os.ReadFile(path.Join("testdata", "prepare-chained-kuma-config", "10-calico.conflist")) - Expect(kubeconfig).To(matchers.MatchGoldenJSON(path.Join("testdata", "prepare-chained-kuma-config", "10-calico.conflist.golden"))) + Describe("PrepareKumaCniConfig", func() { + It("should successfully prepare chained kuma CNI file", func() { + // given + mockServiceAccountPath := filepath.Join("testdata", "prepare-chained-kuma-config") + ic := InstallerConfig{ + CniNetworkConfig: kumaCniConfigTemplate, + MountedCniNetDir: mockServiceAccountPath, + HostCniNetDir: "/foo/bar", + KubeconfigName: "ZZZ-kuma-cni-kubeconfig", + KubernetesServiceAccountTokenPath: filepath.Join(mockServiceAccountPath, "token"), + CniConfName: "10-calico.conflist", + ChainedCniPlugin: true, + } + + // when + err := ic.PrepareKumaCniConfig(context.Background()) + + // then + Expect(err).To(Not(HaveOccurred())) + // and + kubeconfig, _ := os.ReadFile(filepath.Join("testdata", "prepare-chained-kuma-config", "10-calico.conflist")) + Expect(kubeconfig).To(matchers.MatchGoldenJSON(filepath.Join("testdata", "prepare-chained-kuma-config", "10-calico.conflist.golden"))) + }) + + It("should successfully prepare standalone kuma CNI file", func() { + // given + mockServiceAccountPath := filepath.Join("testdata", "prepare-standalone-kuma-config") + ic := InstallerConfig{ + CniNetworkConfig: kumaCniConfigTemplate, + MountedCniNetDir: mockServiceAccountPath, + HostCniNetDir: "/etc/cni/net.d", + KubeconfigName: "ZZZ-kuma-cni-kubeconfig", + KubernetesServiceAccountTokenPath: filepath.Join(mockServiceAccountPath, "token"), + CniConfName: "kuma-cni.conf", + ChainedCniPlugin: false, + } + + // when + err := ic.PrepareKumaCniConfig(context.Background()) + + // then + Expect(err).To(Not(HaveOccurred())) + // and + kubeconfig, _ := os.ReadFile(filepath.Join("testdata", "prepare-standalone-kuma-config", "kuma-cni.conf")) + Expect(kubeconfig).To(matchers.MatchGoldenJSON(filepath.Join("testdata", "prepare-standalone-kuma-config", "kuma-cni.conf.golden"))) + }) }) - It("should successfully prepare standalone kuma cni file", func() { - // given - mockServiceAccountPath := path.Join("testdata", "prepare-standalone-kuma-config") - ic := InstallerConfig{ - CniNetworkConfig: kumaCniConfigTemplate, - MountedCniNetDir: path.Join("testdata", "prepare-standalone-kuma-config"), - KubeconfigName: "ZZZ-kuma-cni-kubeconfig", - CniConfName: "kuma-cni.conf", - ChainedCniPlugin: false, - } - - // when - err := prepareKumaCniConfig(&ic, mockServiceAccountPath) - - // then - Expect(err).To(Not(HaveOccurred())) - // and - kubeconfig, _ := os.ReadFile(path.Join("testdata", "prepare-standalone-kuma-config", "kuma-cni.conf")) - Expect(kubeconfig).To(matchers.MatchGoldenJSON(path.Join("testdata", "prepare-standalone-kuma-config", "kuma-cni.conf.golden"))) + Context("CheckInstall", func() { + It("should not return an error when a file is a conflist file with kuma-cni installed", func() { + // given + ic := InstallerConfig{ + MountedCniNetDir: "testdata", + CniConfName: "10-flannel-cni-injected.conf", + ChainedCniPlugin: true, + } + + // when + err := ic.CheckInstall() + + // then + Expect(err).To(Not(HaveOccurred())) + }) + + It("should not return an error when a file is a conf file with kuma-cni", func() { + // given + ic := InstallerConfig{ + MountedCniNetDir: "testdata", + CniConfName: "10-kuma-cni.conf", + ChainedCniPlugin: false, + } + + // when + err := ic.CheckInstall() + + // then + Expect(err).To(Not(HaveOccurred())) + }) + + It("should return an error when a file does not have kuma-cni installed", func() { + // given + ic := InstallerConfig{ + MountedCniNetDir: "testdata", + CniConfName: "10-flannel.conf", + ChainedCniPlugin: false, + } + + // when + err := ic.CheckInstall() + + // then + Expect(err).To(HaveOccurred()) + }) }) }) diff --git a/app/cni/pkg/install/main.go b/app/cni/pkg/install/main.go index 2295e48fac70..8836b1c65211 100644 --- a/app/cni/pkg/install/main.go +++ b/app/cni/pkg/install/main.go @@ -3,9 +3,9 @@ package install import ( "bytes" "context" + std_errors "errors" "os" "os/signal" - "path" "path/filepath" "strings" "syscall" @@ -14,19 +14,18 @@ import ( "github.com/natefinch/atomic" "github.com/pkg/errors" "github.com/sethvargo/go-retry" - "go.uber.org/multierr" kuma_log "github.com/kumahq/kuma/pkg/log" "github.com/kumahq/kuma/pkg/util/files" ) const ( - kumaCniBinaryPath = "/opt/cni/bin/kuma-cni" - primaryBinDir = "/host/opt/cni/bin" - secondaryBinDir = "/host/secondary-bin-dir" - serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount" - readyFilePath = "/tmp/ready" - defaultLogName = "install-cni" + binaryName = "kuma-cni" + binaryPath = "/opt/cni/bin/" + binaryName + primaryBinDir = "/host/opt/cni/bin" + secondaryBinDir = "/host/secondary-bin-dir" + readyFilePath = "/tmp/ready" + defaultLogName = "install-cni" ) var log = CreateNewLogger(defaultLogName, kuma_log.DebugLevel) @@ -42,7 +41,7 @@ func cleanup(ic *InstallerConfig) { } else { log.V(1).Info("removed existing binaries") } - if err := revertConfig(ic.MountedCniNetDir, ic.CniConfName, ic.ChainedCniPlugin); err != nil { + if err := revertConfig(ic.MountedCniNetDir+"/"+ic.CniConfName, ic.ChainedCniPlugin); err != nil { log.Error(err, "could not revert config") } else { log.V(1).Info("reverted config") @@ -71,166 +70,178 @@ func removeKubeconfig(mountedCniNetDir, kubeconfigName string) error { return nil } -func revertConfig(mountedCniNetDir, cniConfName string, chainedCniPlugin bool) error { - configPath := mountedCniNetDir + "/" + cniConfName - +func revertConfig(configPath string, chained bool) error { if !files.FileExists(configPath) { log.Info("no need to revert config - file does not exist", "configPath", configPath) return nil } - if chainedCniPlugin { + if chained { contents, err := os.ReadFile(configPath) if err != nil { - return errors.Wrap(err, "couldn't read cni conf file") + return errors.Wrap(err, "could not read cni conf file") } + newContents, err := revertConfigContents(contents) if err != nil { return errors.Wrap(err, "could not revert config contents") } - err = atomic.WriteFile(configPath, bytes.NewReader(newContents)) - if err != nil { + + if err := atomic.WriteFile(configPath, bytes.NewReader(newContents)); err != nil { return errors.Wrap(err, "could not write new conf") } - } else { - err := os.Remove(configPath) - if err != nil { - return errors.Wrap(err, "couldn't remove cni conf file") - } + + return nil + } + + if err := os.Remove(configPath); err != nil { + return errors.Wrap(err, "could not remove cni conf file") } return nil } -func install(ic *InstallerConfig) error { - if err := copyBinaries(); err != nil { - return errors.Wrap(err, "could not copy binary files") +func install(ctx context.Context, ic *InstallerConfig) error { + if err := ic.PrepareKubeconfig(); err != nil { + return errors.Wrap(err, "failed to prepare kubeconfig") + } + + if err := ic.CheckInstall(); err == nil { + log.Info("Kuma CNI is already installed and configured") + return nil + } else { + log.Info("no valid installation found, will proceed with installation", "error", err) } - if err := prepareKubeconfig(ic, serviceAccountPath); err != nil { - return errors.Wrap(err, "could not prepare kubeconfig") + if err := copyBinaries(); err != nil { + return errors.Wrap(err, "failed to copy binary files") } - if err := prepareKumaCniConfig(ic, serviceAccountPath); err != nil { - return errors.Wrap(err, "could not prepare kuma cni config") + if err := ic.PrepareKumaCniConfig(ctx); err != nil { + return errors.Wrap(err, "failed to prepare Kuma CNI configuration") } + log.Info("Kuma CNI installation completed successfully") + return nil } -func setupChainedPlugin(mountedCniNetDir, cniConfName, kumaCniConfig string) error { - resolvedName := cniConfName +func setupChainedPlugin(ctx context.Context, mountedCniNetDir, cniConfName, kumaCniConfig string) error { extension := filepath.Ext(cniConfName) - if !files.FileExists(mountedCniNetDir+"/"+cniConfName) && extension == ".conf" && files.FileExists(mountedCniNetDir+"/"+cniConfName+"list") { + pathConf := filepath.Join(mountedCniNetDir, cniConfName) + pathConflist := filepath.Join(mountedCniNetDir, cniConfName+"list") + + resolvedName := cniConfName + if !files.FileExists(pathConf) && extension == ".conf" && files.FileExists(pathConflist) { resolvedName = cniConfName + "list" } - cniConfPath := path.Join(mountedCniNetDir, resolvedName) + cniConfPath := filepath.Join(mountedCniNetDir, resolvedName) + backoff := retry.WithMaxDuration(5*time.Minute, retry.NewConstant(time.Second)) - err := retry.Do(context.Background(), backoff, func(ctx context.Context) error { - if !files.FileExists(cniConfPath) { - err := errors.Errorf("CNI config '%s' not found.", cniConfPath) - log.Error(err, "error chaining Kuma CNI config, will retry...") - return retry.RetryableError(err) + err := retry.Do(ctx, backoff, func(ctx context.Context) error { + if files.FileExists(cniConfPath) { + return nil } - return nil + + err := errors.Errorf("cni config '%s' not found", cniConfPath) + log.Error(err, "error chaining CNI config, retrying...") + return retry.RetryableError(err) }) if err != nil { - return err + return errors.Wrap(err, "failed to ensure CNI config presence") } hostCniConfig, err := os.ReadFile(cniConfPath) if err != nil { - return err + return errors.Wrap(err, "failed to read CNI config file") } marshaled, err := transformJsonConfig(kumaCniConfig, hostCniConfig) if err != nil { - return err + return errors.Wrap(err, "failed to transform JSON config") + } + + log.V(1).Info("resulting config generated", "config", string(marshaled)) + log.Info("chaining CNI config, updating config file", "file", cniConfPath) + + if err := atomic.WriteFile(cniConfPath, bytes.NewReader(marshaled)); err != nil { + return errors.Wrap(err, "failed to write updated CNI config") } - log.V(1).Info("resulting config", "config", string(marshaled)) - log.Info("chaining Kuma CNI config. Updating CNI config file", "file", mountedCniNetDir+"/"+resolvedName) - err = atomic.WriteFile(mountedCniNetDir+"/"+resolvedName, bytes.NewReader(marshaled)) - return err + return nil } func copyBinaries() error { - dirs := []string{primaryBinDir, secondaryBinDir} - writtenOnce := false - allErrors := errors.New("combined errors for copying binaries") - for _, dir := range dirs { + var errs error + + for _, dir := range []string{primaryBinDir, secondaryBinDir} { err := tryWritingToDir(dir) - if err != nil { - allErrors = multierr.Append(allErrors, err) - log.Info("writing to dir failed", "dir", dir) - continue + if err == nil { + log.Info("successfully wrote kuma CNI binaries", "directory", dir) + return nil } - log.Info("wrote kuma CNI binaries", "dir", dir) - writtenOnce = true - } + errs = std_errors.Join( + errs, + errors.Wrapf(err, "failed to write binaries to directory %s", dir), + ) - if !writtenOnce { - return allErrors + log.Info("failed to write binaries", "directory", dir) } - return nil + + return errs } func tryWritingToDir(dir string) error { if err := files.IsDirWriteable(dir); err != nil { - return errors.Wrap(err, "directory is not writeable") - } - file, err := os.Open(kumaCniBinaryPath) - if err != nil { - return errors.Wrap(err, "can't open kuma-cni file") + return errors.Wrap(err, "directory is not writable") } - stat, err := os.Stat(kumaCniBinaryPath) + file, err := os.Open(binaryPath) if err != nil { - return errors.Wrap(err, "can't stat kuma-cni file") + return errors.Wrap(err, "unable to open CNI binary file") } - log.V(1).Info("cni binary file permissions", "permissions", int(stat.Mode()), "path", kumaCniBinaryPath) + defer file.Close() - destination := dir + "/kuma-cni" - err = atomic.WriteFile(destination, file) + stat, err := file.Stat() if err != nil { - return errors.Wrap(err, "can't atomically write kuma-cni file") + return errors.Wrap(err, "unable to stat CNI binary file") } - err = os.Chmod(destination, stat.Mode()|0o111) - if err != nil { - return errors.Wrap(err, "can't chmod kuma-cni file") + log.V(1).Info("CNI binary file permissions", "permissions", int(stat.Mode()), "path", binaryPath) + + destination := filepath.Join(dir, binaryName) + + if err := atomic.WriteFile(destination, file); err != nil { + return errors.Wrap(err, "unable to atomically write CNI binary file") } - if err != nil { - return errors.Wrap(err, "can't atomically write cni file") + if err := os.Chmod(destination, stat.Mode()|0o111); err != nil { + return errors.Wrap(err, "unable to chmod CNI binary file") } return nil } -func Run() { +func Run(ctx context.Context) { installerConfig, err := loadInstallerConfig() if err != nil { log.Error(err, "error occurred during config loading") os.Exit(1) } - err = SetLogLevel(&log, installerConfig.CniLogLevel, defaultLogName) - if err != nil { + if err := SetLogLevel(&log, installerConfig.CniLogLevel, defaultLogName); err != nil { log.Error(err, "error occurred during setting the log level") os.Exit(2) } - err = install(installerConfig) - if err != nil { + if err := install(ctx, installerConfig); err != nil { log.Error(err, "error occurred during cni installation") os.Exit(3) } - err = atomic.WriteFile(readyFilePath, strings.NewReader("")) - if err != nil { + if err := atomic.WriteFile(readyFilePath, strings.NewReader("")); err != nil { log.Error(err, "unable to mark as ready") os.Exit(4) } @@ -255,8 +266,7 @@ func runLoop(ic *InstallerConfig) error { case <-osSignals: return nil case <-time.After(time.Duration(ic.CfgCheckInterval) * time.Second): - err := checkInstall(ic.MountedCniNetDir+"/"+ic.CniConfName, ic.ChainedCniPlugin) - if err != nil { + if err := ic.CheckInstall(); err != nil { return err } } diff --git a/app/cni/pkg/install/main_test.go b/app/cni/pkg/install/main_test.go deleted file mode 100644 index 1de6782f30f8..000000000000 --- a/app/cni/pkg/install/main_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package install - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -const expectedKubeconfig = `# Kubeconfig file for kuma CNI plugin. -apiVersion: v1 -kind: Config -clusters: -- name: local - cluster: - server: https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:3000 - certificate-authority-data: YWJjCg== -users: -- name: kuma-cni - user: - token: token -contexts: -- name: kuma-cni-context - context: - cluster: local - user: kuma-cni -current-context: kuma-cni-context` - -var _ = Describe("kubeconfigTemplate", func() { - It("should work properly with unescaped IPv6 addresses", func() { - // when - result := kubeconfigTemplate("https", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", "3000", "token", "YWJjCg==") - - // then - Expect(result).To(Equal(expectedKubeconfig)) - }) -}) diff --git a/app/cni/pkg/install/testdata/prepare-chained-kuma-config/10-calico.conflist b/app/cni/pkg/install/testdata/prepare-chained-kuma-config/10-calico.conflist index 796a426d2f15..459be53c5ee3 100644 --- a/app/cni/pkg/install/testdata/prepare-chained-kuma-config/10-calico.conflist +++ b/app/cni/pkg/install/testdata/prepare-chained-kuma-config/10-calico.conflist @@ -30,7 +30,7 @@ "exclude_namespaces": [ "kuma-system" ], - "kubeconfig": "/ZZZ-kuma-cni-kubeconfig" + "kubeconfig": "/foo/bar/ZZZ-kuma-cni-kubeconfig" }, "log_level": "info", "type": "kuma-cni" diff --git a/app/cni/pkg/install/testdata/prepare-chained-kuma-config/10-calico.conflist.golden b/app/cni/pkg/install/testdata/prepare-chained-kuma-config/10-calico.conflist.golden index e227652f7e56..36a4e9fd70c5 100644 --- a/app/cni/pkg/install/testdata/prepare-chained-kuma-config/10-calico.conflist.golden +++ b/app/cni/pkg/install/testdata/prepare-chained-kuma-config/10-calico.conflist.golden @@ -30,7 +30,7 @@ "exclude_namespaces": [ "kuma-system" ], - "kubeconfig": "/ZZZ-kuma-cni-kubeconfig" + "kubeconfig": "/foo/bar/ZZZ-kuma-cni-kubeconfig" }, "log_level": "info", "type": "kuma-cni" diff --git a/app/cni/pkg/install/testdata/prepare-standalone-kuma-config/kuma-cni.conf b/app/cni/pkg/install/testdata/prepare-standalone-kuma-config/kuma-cni.conf index 695ec18078a3..1f98dad95898 100644 --- a/app/cni/pkg/install/testdata/prepare-standalone-kuma-config/kuma-cni.conf +++ b/app/cni/pkg/install/testdata/prepare-standalone-kuma-config/kuma-cni.conf @@ -2,7 +2,7 @@ "type": "kuma-cni", "log_level": "info", "kubernetes": { - "kubeconfig": "/ZZZ-kuma-cni-kubeconfig", + "kubeconfig": "/etc/cni/net.d/ZZZ-kuma-cni-kubeconfig", "cni_bin_dir": "/opt/cni/bin", "exclude_namespaces": [ "kuma-system" diff --git a/app/cni/pkg/install/testdata/prepare-standalone-kuma-config/kuma-cni.conf.golden b/app/cni/pkg/install/testdata/prepare-standalone-kuma-config/kuma-cni.conf.golden index 45f02fc9459e..6b99a0d97aa4 100644 --- a/app/cni/pkg/install/testdata/prepare-standalone-kuma-config/kuma-cni.conf.golden +++ b/app/cni/pkg/install/testdata/prepare-standalone-kuma-config/kuma-cni.conf.golden @@ -2,7 +2,7 @@ "type": "kuma-cni", "log_level": "info", "kubernetes": { - "kubeconfig": "/ZZZ-kuma-cni-kubeconfig", + "kubeconfig": "/etc/cni/net.d/ZZZ-kuma-cni-kubeconfig", "cni_bin_dir": "/opt/cni/bin", "exclude_namespaces": [ "kuma-system" diff --git a/app/cni/pkg/install/validation.go b/app/cni/pkg/install/validation.go index b25439ca1cc4..3ab8a7bb1db2 100644 --- a/app/cni/pkg/install/validation.go +++ b/app/cni/pkg/install/validation.go @@ -2,93 +2,58 @@ package install import ( "github.com/pkg/errors" - - "github.com/kumahq/kuma/pkg/util/files" ) func lookForValidConfig(files []string, checkerFn func(string) error) (string, bool) { for _, file := range files { - err := checkerFn(file) - if err != nil { + if err := checkerFn(file); err != nil { log.Info("error occurred testing config file", "file", file) - } else { - return file, true + continue } + + return file, true } + return "", false } func isValidConfFile(file string) error { parsed, err := parseFileToHashMap(file) if err != nil { - return errors.Wrap(err, "could not unmarshal conf file") + return errors.Wrap(err, "failed to parse configuration file") } - configType, ok := parsed["type"] - if ok { - log.V(1).Info("config valid", "file", file, "type", configType) + if configType, ok := parsed["type"]; ok { + log.V(1).Info("configuration validated", "file", file, "type", configType) return nil } - return errors.Errorf(`config file %v not valid - does not contain "type" field`, file) + + return errors.Errorf(`configuration file "%s" missing "type" field`, file) } func isValidConflistFile(file string) error { parsed, err := parseFileToHashMap(file) if err != nil { - return errors.Wrap(err, "could not unmarshal conflist file") + return errors.Wrap(err, "failed to parse conflist file") } - configName, hasName := parsed["name"] - plugins, hasPlugins := parsed["plugins"] - if hasName && hasPlugins { - log.V(1).Info("config valid", "file", file, "name", configName, "plugins", plugins) - return nil - } - - return errors.Errorf(`config file %v not valid - does not contain "name" and "plugin" fields`, file) -} + var missingFields []string -func checkInstall(cniConfPath string, isPluginChained bool) error { - if !files.FileExists(cniConfPath) { - return errors.New("cni config file does not exist") + configName, ok := parsed["name"] + if !ok { + missingFields = append(missingFields, "name") } - parsed, err := parseFileToHashMap(cniConfPath) - if err != nil { - return err + plugins, ok := parsed["plugins"] + if !ok { + missingFields = append(missingFields, "plugins") } - if isPluginChained { - err := isValidConflistFile(cniConfPath) - if err != nil { - return errors.Wrap(err, "chained plugin requires a valid conflist file") - } - plugins, err := getPluginsArray(parsed) - if err != nil { - return err - } - index, err := findKumaCniConfigIndex(plugins) - if err != nil { - return err - } - if index >= 0 { - return nil - } else { - return errors.New("chained plugin config file does not contain kuma-cni plugin") - } - } else { - err := isValidConfFile(cniConfPath) - if err != nil { - return errors.Wrap(err, "standalone plugin requires a valid conf file") - } - pluginType, ok := parsed["type"] - if !ok { - return errors.New("cni config was modified and does not have a type") - } - if pluginType == "kuma-cni" { - return nil - } else { - return errors.New("config file does not contain kuma-cni configuration") - } + if len(missingFields) > 0 { + return errors.Errorf("conflist file %s missing required fields: %+v", file, missingFields) } + + log.V(1).Info("conflist validated", "file", file, "name", configName, "plugins", plugins) + + return nil } diff --git a/app/cni/pkg/install/validation_test.go b/app/cni/pkg/install/validation_test.go index 992a09c050c2..e117ac09effc 100644 --- a/app/cni/pkg/install/validation_test.go +++ b/app/cni/pkg/install/validation_test.go @@ -35,24 +35,4 @@ var _ = Describe("validation", func() { Expect(err).To(HaveOccurred()) }) }) - - Context("checkInstall", func() { - It("should not return error when a file is a conflist file with kuma-cni installed", func() { - err := checkInstall(path.Join("testdata", "10-flannel-cni-injected.conf"), true) - - Expect(err).To(Not(HaveOccurred())) - }) - - It("should return true when a file is a conf file with kuma-cni", func() { - err := checkInstall(path.Join("testdata", "10-kuma-cni.conf"), false) - - Expect(err).To(Not(HaveOccurred())) - }) - - It("should return false when a file does not have kuma cni installed", func() { - err := checkInstall(path.Join("testdata", "10-flannel.conf"), false) - - Expect(err).To(HaveOccurred()) - }) - }) })