diff --git a/CHANGELOG.md b/CHANGELOG.md index 8643d3dce7..99a548a019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ IMPROVEMENTS: * Upgrade Docker image Alpine version from 3.14 to 3.15. [[GH-1058](https://github.com/hashicorp/consul-k8s/pull/1058)] * Helm * API Gateway: Allow controller to read Kubernetes namespaces in order to determine if route is allowed for gateway. [[GH-1092](https://github.com/hashicorp/consul-k8s/pull/1092)] +* CLI + * Enable users to set up secondary clusters with existing federation secrets. [[GH-1126](https://github.com/hashicorp/consul-k8s/pull/1126)] BUG FIXES: * Helm diff --git a/cli/cmd/install/install.go b/cli/cmd/install/install.go index f33b044ef8..9aa21737fc 100644 --- a/cli/cmd/install/install.go +++ b/cli/cmd/install/install.go @@ -14,6 +14,8 @@ import ( "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/consul-k8s/cli/config" "github.com/hashicorp/consul-k8s/cli/helm" + "github.com/hashicorp/consul-k8s/cli/release" + "github.com/hashicorp/consul-k8s/cli/validation" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart/loader" helmCLI "helm.sh/helm/v3/pkg/cli" @@ -176,19 +178,6 @@ func (c *Command) init() { c.Init() } -type helmValues struct { - Global globalValues `yaml:"global"` -} - -type globalValues struct { - EnterpriseLicense enterpriseLicense `yaml:"enterpriseLicense"` -} - -type enterpriseLicense struct { - SecretName string `yaml:"secretName"` - SecretKey string `yaml:"secretKey"` -} - // Run installs Consul into a Kubernetes cluster. func (c *Command) Run(args []string) int { c.once.Do(c.init) @@ -268,13 +257,6 @@ func (c *Command) Run(args []string) int { } c.UI.Output("No existing Consul persistent volume claims found", terminal.WithSuccessStyle()) - // Ensure there's no previous bootstrap secret lying around. - if err := c.checkForPreviousSecrets(); err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 - } - c.UI.Output("No existing Consul secrets found", terminal.WithSuccessStyle()) - // Handle preset, value files, and set values logic. vals, err := c.mergeValuesFlagsWithPrecedence(settings) if err != nil { @@ -287,16 +269,29 @@ func (c *Command) Run(args []string) int { return 1 } - var v helmValues - err = yaml.Unmarshal(valuesYaml, &v) + var values helm.Values + err = yaml.Unmarshal(valuesYaml, &values) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } + release := release.Release{ + Name: "consul", + Namespace: c.flagNamespace, + Configuration: values, + } + + msg, err := c.checkForPreviousSecrets(release) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + c.UI.Output(msg, terminal.WithSuccessStyle()) + // If an enterprise license secret was provided, check that the secret exists and that the enterprise Consul image is set. - if v.Global.EnterpriseLicense.SecretName != "" { - if err := c.checkValidEnterprise(v.Global.EnterpriseLicense.SecretName); err != nil { + if values.Global.EnterpriseLicense.SecretName != "" { + if err := c.checkValidEnterprise(release.Configuration.Global.EnterpriseLicense.SecretName); err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } @@ -420,21 +415,45 @@ func (c *Command) checkForPreviousPVCs() error { return nil } -// checkForPreviousSecrets checks for the bootstrap token and returns an error if found. -func (c *Command) checkForPreviousSecrets() error { - secrets, err := c.kubernetes.CoreV1().Secrets("").List(c.Ctx, metav1.ListOptions{LabelSelector: common.CLILabelKey + "=" + common.CLILabelValue}) +// checkForPreviousSecrets checks for Consul secrets that exist in the cluster +// and returns a message if the secret configuration is ok or an error if +// the secret configuration could cause a conflict. +func (c *Command) checkForPreviousSecrets(release release.Release) (string, error) { + secrets, err := validation.ListConsulSecrets(c.Ctx, c.kubernetes) if err != nil { - return fmt.Errorf("error listing secrets: %s", err) + return "", fmt.Errorf("Error listing Consul secrets: %s", err) + } + + // If the Consul configuration is a secondary DC, only one secret should + // exist, the Consul federation secret. + fedSecret := release.Configuration.Global.Acls.ReplicationToken.SecretName + if release.ShouldExpectFederationSecret() { + if len(secrets.Items) == 1 && secrets.Items[0].Name == fedSecret { + return fmt.Sprintf("Found secret %s for Consul federation.", fedSecret), nil + } else if len(secrets.Items) == 0 { + return "", fmt.Errorf("Missing secret %s for Consul federation.\n"+ + "Please refer to the Consul Secondary Cluster configuration docs:\nhttps://www.consul.io/docs/k8s/installation/multi-cluster/kubernetes#secondary-cluster-s", fedSecret) + } } - for _, secret := range secrets.Items { - // future TODO: also check for federation secret - if secret.ObjectMeta.Labels[common.CLILabelKey] == common.CLILabelValue { - return fmt.Errorf("found Consul secret from previous installation: %q in namespace %q. Use the command `kubectl delete secret %s --namespace %s` to delete", - secret.Name, secret.Namespace, secret.Name, secret.Namespace) + + // If not a secondary DC for federation, no Consul secrets should exist. + if len(secrets.Items) > 0 { + // Nicely format the delete commands for existing Consul secrets. + namespacedSecrets := make(map[string][]string) + for _, secret := range secrets.Items { + namespacedSecrets[secret.Namespace] = append(namespacedSecrets[secret.Namespace], secret.Name) + } + + var deleteCmds string + for namespace, secretNames := range namespacedSecrets { + deleteCmds += fmt.Sprintf("kubectl delete secret %s --namespace %s\n", strings.Join(secretNames, " "), namespace) } + + return "", fmt.Errorf("Found Consul secrets, possibly from a previous installation.\n"+ + "Delete existing Consul secrets from Kubernetes:\n\n%s", deleteCmds) } - return nil + return "No existing Consul secrets found.", nil } // mergeValuesFlagsWithPrecedence is responsible for merging all the values to determine the values file for the diff --git a/cli/cmd/install/install_test.go b/cli/cmd/install/install_test.go index b38af685fc..ad91e85b30 100644 --- a/cli/cmd/install/install_test.go +++ b/cli/cmd/install/install_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/helm" + "github.com/hashicorp/consul-k8s/cli/release" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" @@ -49,33 +51,102 @@ func TestCheckForPreviousPVCs(t *testing.T) { } func TestCheckForPreviousSecrets(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - secret := &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-consul-bootstrap-acl-token", - Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + t.Parallel() + + cases := map[string]struct { + helmValues helm.Values + secret *v1.Secret + expectMsg bool + expectErr bool + }{ + "No secrets, none expected": { + helmValues: helm.Values{}, + secret: nil, + expectMsg: true, + expectErr: false, + }, + "Non-Consul secrets, none expected": { + helmValues: helm.Values{}, + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "non-consul-secret", + }, + }, + expectMsg: true, + expectErr: false, + }, + "Consul secrets, none expected": { + helmValues: helm.Values{}, + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-secret", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + }, + expectMsg: false, + expectErr: true, + }, + "Federation secret, expected": { + helmValues: helm.Values{ + Global: helm.Global{ + Datacenter: "dc2", + Federation: helm.Federation{ + Enabled: true, + PrimaryDatacenter: "dc1", + CreateFederationSecret: false, + }, + Acls: helm.Acls{ + ReplicationToken: helm.ReplicationToken{ + SecretName: "consul-federation", + }, + }, + }, + }, + secret: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-federation", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + }, + expectMsg: true, + expectErr: false, + }, + "No federation secret, but expected": { + helmValues: helm.Values{ + Global: helm.Global{ + Datacenter: "dc2", + Federation: helm.Federation{ + Enabled: true, + PrimaryDatacenter: "dc1", + CreateFederationSecret: false, + }, + Acls: helm.Acls{ + ReplicationToken: helm.ReplicationToken{ + SecretName: "consul-federation", + }, + }, + }, + }, + secret: nil, + expectMsg: false, + expectErr: true, }, } - c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret, metav1.CreateOptions{}) - err := c.checkForPreviousSecrets() - require.Error(t, err) - require.Contains(t, err.Error(), "found Consul secret from previous installation") - // Clear out the client and make sure the check now passes. - c.kubernetes = fake.NewSimpleClientset() - err = c.checkForPreviousSecrets() - require.NoError(t, err) + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() - // Add a new irrelevant secret and make sure the check continues to pass. - secret = &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "irrelevant-secret", - }, + c.kubernetes.CoreV1().Secrets("consul").Create(context.Background(), tc.secret, metav1.CreateOptions{}) + + release := release.Release{Configuration: tc.helmValues} + msg, err := c.checkForPreviousSecrets(release) + + require.Equal(t, tc.expectMsg, msg != "") + require.Equal(t, tc.expectErr, err != nil) + }) } - c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret, metav1.CreateOptions{}) - err = c.checkForPreviousSecrets() - require.NoError(t, err) } // TestValidateFlags tests the validate flags function. diff --git a/cli/common/utils.go b/cli/common/utils.go index 3d7df71433..e03238bfb0 100644 --- a/cli/common/utils.go +++ b/cli/common/utils.go @@ -74,6 +74,9 @@ func MergeMaps(a, b map[string]interface{}) map[string]interface{} { return out } +// CloseWithError terminates a command and cleans up its resources. +// If termination fails, the error is logged and the process exits with an +// exit code of 1. func CloseWithError(c *BaseCommand) { if err := c.Close(); err != nil { c.Log.Error(err.Error()) diff --git a/cli/helm/values.go b/cli/helm/values.go new file mode 100644 index 0000000000..8969c2a1de --- /dev/null +++ b/cli/helm/values.go @@ -0,0 +1,618 @@ +package helm + +// HACK this is a temporary hard-coded struct. We should actually generate this from our `values.yaml` file. + +// Values is the Helm values that may be set for the Consul Helm Chart. +type Values struct { + Global Global `yaml:"global"` + Server Server `yaml:"server"` + ExternalServers ExternalServers `yaml:"externalServers"` + Client Client `yaml:"client"` + DNS DNS `yaml:"dns"` + UI UI `yaml:"ui"` + SyncCatalog SyncCatalog `yaml:"syncCatalog"` + ConnectInject ConnectInject `yaml:"connectInject"` + Controller Controller `yaml:"controller"` + MeshGateway MeshGateway `yaml:"meshGateway"` + IngressGateways IngressGateways `yaml:"ingressGateways"` + TerminatingGateways TerminatingGateways `yaml:"terminatingGateways"` + APIGateway APIGateway `yaml:"apiGateway"` + WebhookCertManager WebhookCertManager `yaml:"webhookCertManager"` + Prometheus Prometheus `yaml:"prometheus"` + Tests Tests `yaml:"tests"` +} + +type NodePort struct { + RPC interface{} `yaml:"rpc"` + Serf interface{} `yaml:"serf"` + HTTPS interface{} `yaml:"https"` +} + +type AdminPartitionsService struct { + Type string `yaml:"type"` + NodePort NodePort `yaml:"nodePort"` + Annotations interface{} `yaml:"annotations"` +} + +type AdminPartitions struct { + Enabled bool `yaml:"enabled"` + Name string `yaml:"name"` + Service AdminPartitionsService `yaml:"service"` +} + +type Ca struct { + SecretName string `yaml:"secretName"` + SecretKey string `yaml:"secretKey"` +} + +type ConnectCA struct { + Address string `yaml:"address"` + AuthMethodPath string `yaml:"authMethodPath"` + RootPKIPath string `yaml:"rootPKIPath"` + IntermediatePKIPath string `yaml:"intermediatePKIPath"` + AdditionalConfig string `yaml:"additionalConfig"` +} + +type Vault struct { + Enabled bool `yaml:"enabled"` + ConsulServerRole string `yaml:"consulServerRole"` + ConsulClientRole string `yaml:"consulClientRole"` + ManageSystemACLsRole string `yaml:"manageSystemACLsRole"` + AgentAnnotations interface{} `yaml:"agentAnnotations"` + ConsulCARole string `yaml:"consulCARole"` + Ca Ca `yaml:"ca"` + ConnectCA ConnectCA `yaml:"connectCA"` +} + +type SecretsBackend struct { + Vault Vault `yaml:"vault"` +} + +type GossipEncryption struct { + AutoGenerate bool `yaml:"autoGenerate"` + SecretName string `yaml:"secretName"` + SecretKey string `yaml:"secretKey"` +} + +type CaCert struct { + SecretName interface{} `yaml:"secretName"` + SecretKey interface{} `yaml:"secretKey"` +} + +type CaKey struct { + SecretName interface{} `yaml:"secretName"` + SecretKey interface{} `yaml:"secretKey"` +} + +type TLS struct { + Enabled bool `yaml:"enabled"` + EnableAutoEncrypt bool `yaml:"enableAutoEncrypt"` + ServerAdditionalDNSSANs []interface{} `yaml:"serverAdditionalDNSSANs"` + ServerAdditionalIPSANs []interface{} `yaml:"serverAdditionalIPSANs"` + Verify bool `yaml:"verify"` + HTTPSOnly bool `yaml:"httpsOnly"` + CaCert CaCert `yaml:"caCert"` + CaKey CaKey `yaml:"caKey"` +} + +type BootstrapToken struct { + SecretName interface{} `yaml:"secretName"` + SecretKey interface{} `yaml:"secretKey"` +} + +type ReplicationToken struct { + SecretName string `yaml:"secretName"` + SecretKey string `yaml:"secretKey"` +} + +type Acls struct { + ManageSystemACLs bool `yaml:"manageSystemACLs"` + BootstrapToken BootstrapToken `yaml:"bootstrapToken"` + CreateReplicationToken bool `yaml:"createReplicationToken"` + ReplicationToken ReplicationToken `yaml:"replicationToken"` +} + +type EnterpriseLicense struct { + SecretName string `yaml:"secretName"` + SecretKey string `yaml:"secretKey"` + EnableLicenseAutoload bool `yaml:"enableLicenseAutoload"` +} + +type Federation struct { + Enabled bool `yaml:"enabled"` + CreateFederationSecret bool `yaml:"createFederationSecret"` + PrimaryDatacenter string `yaml:"primaryDatacenter"` + PrimaryGateways []interface{} `yaml:"primaryGateways"` +} + +type GlobalMetrics struct { + Enabled bool `yaml:"enabled"` + EnableAgentMetrics bool `yaml:"enableAgentMetrics"` + AgentMetricsRetentionTime string `yaml:"agentMetricsRetentionTime"` + EnableGatewayMetrics bool `yaml:"enableGatewayMetrics"` +} + +type Requests struct { + Memory string `yaml:"memory"` + CPU string `yaml:"cpu"` +} + +type Limits struct { + Memory string `yaml:"memory"` + CPU string `yaml:"cpu"` +} + +type Resources struct { + Requests Requests `yaml:"requests"` + Limits Limits `yaml:"limits"` +} + +type ConsulSidecarContainer struct { + Resources Resources `yaml:"resources"` +} + +type Openshift struct { + Enabled bool `yaml:"enabled"` +} + +type Global struct { + Enabled bool `yaml:"enabled"` + LogLevel string `yaml:"logLevel"` + LogJSON bool `yaml:"logJSON"` + Name interface{} `yaml:"name"` + Domain string `yaml:"domain"` + AdminPartitions AdminPartitions `yaml:"adminPartitions"` + Image string `yaml:"image"` + ImagePullSecrets []interface{} `yaml:"imagePullSecrets"` + ImageK8S string `yaml:"imageK8S"` + Datacenter string `yaml:"datacenter"` + EnablePodSecurityPolicies bool `yaml:"enablePodSecurityPolicies"` + SecretsBackend SecretsBackend `yaml:"secretsBackend"` + GossipEncryption GossipEncryption `yaml:"gossipEncryption"` + Recursors []interface{} `yaml:"recursors"` + TLS TLS `yaml:"tls"` + EnableConsulNamespaces bool `yaml:"enableConsulNamespaces"` + Acls Acls `yaml:"acls"` + EnterpriseLicense EnterpriseLicense `yaml:"enterpriseLicense"` + Federation Federation `yaml:"federation"` + Metrics GlobalMetrics `yaml:"metrics"` + ConsulSidecarContainer ConsulSidecarContainer `yaml:"consulSidecarContainer"` + ImageEnvoy string `yaml:"imageEnvoy"` + Openshift Openshift `yaml:"openshift"` +} + +type ServerCert struct { + SecretName interface{} `yaml:"secretName"` +} + +type Serflan struct { + Port int `yaml:"port"` +} + +type Ports struct { + Serflan Serflan `yaml:"serflan"` +} + +type ServiceAccount struct { + Annotations interface{} `yaml:"annotations"` +} + +type SecurityContext struct { + RunAsNonRoot bool `yaml:"runAsNonRoot"` + RunAsGroup int `yaml:"runAsGroup"` + RunAsUser int `yaml:"runAsUser"` + FsGroup int `yaml:"fsGroup"` +} + +type ContainerSecurityContext struct { + Server interface{} `yaml:"server"` +} + +type DisruptionBudget struct { + Enabled bool `yaml:"enabled"` + MaxUnavailable interface{} `yaml:"maxUnavailable"` +} + +type ServerService struct { + Annotations interface{} `yaml:"annotations"` +} + +type ExtraEnvironmentVars struct { +} + +type Server struct { + Enabled string `yaml:"enabled"` + Image interface{} `yaml:"image"` + Replicas int `yaml:"replicas"` + BootstrapExpect interface{} `yaml:"bootstrapExpect"` + ServerCert ServerCert `yaml:"serverCert"` + ExposeGossipAndRPCPorts bool `yaml:"exposeGossipAndRPCPorts"` + Ports Ports `yaml:"ports"` + Storage string `yaml:"storage"` + StorageClass interface{} `yaml:"storageClass"` + Connect bool `yaml:"connect"` + ServiceAccount ServiceAccount `yaml:"serviceAccount"` + Resources Resources `yaml:"resources"` + SecurityContext SecurityContext `yaml:"securityContext"` + ContainerSecurityContext ContainerSecurityContext `yaml:"containerSecurityContext"` + UpdatePartition int `yaml:"updatePartition"` + DisruptionBudget DisruptionBudget `yaml:"disruptionBudget"` + ExtraConfig string `yaml:"extraConfig"` + ExtraVolumes []interface{} `yaml:"extraVolumes"` + ExtraContainers []interface{} `yaml:"extraContainers"` + Affinity string `yaml:"affinity"` + Tolerations string `yaml:"tolerations"` + TopologySpreadConstraints string `yaml:"topologySpreadConstraints"` + NodeSelector interface{} `yaml:"nodeSelector"` + PriorityClassName string `yaml:"priorityClassName"` + ExtraLabels interface{} `yaml:"extraLabels"` + Annotations interface{} `yaml:"annotations"` + Service ServerService `yaml:"service"` + ExtraEnvironmentVars ExtraEnvironmentVars `yaml:"extraEnvironmentVars"` +} + +type ExternalServers struct { + Enabled bool `yaml:"enabled"` + Hosts []interface{} `yaml:"hosts"` + HTTPSPort int `yaml:"httpsPort"` + TLSServerName interface{} `yaml:"tlsServerName"` + UseSystemRoots bool `yaml:"useSystemRoots"` + K8SAuthMethodHost interface{} `yaml:"k8sAuthMethodHost"` +} + +type NodeMeta struct { + PodName string `yaml:"pod-name"` + HostIP string `yaml:"host-ip"` +} + +type ClientContainerSecurityContext struct { + Client interface{} `yaml:"client"` + ACLInit interface{} `yaml:"aclInit"` + TLSInit interface{} `yaml:"tlsInit"` +} + +type ConfigSecret struct { + SecretName interface{} `yaml:"secretName"` + SecretKey interface{} `yaml:"secretKey"` +} + +type SnapshotAgent struct { + Enabled bool `yaml:"enabled"` + Replicas int `yaml:"replicas"` + ConfigSecret ConfigSecret `yaml:"configSecret"` + ServiceAccount ServiceAccount `yaml:"serviceAccount"` + Resources Resources `yaml:"resources"` + CaCert interface{} `yaml:"caCert"` +} + +type Client struct { + Enabled string `yaml:"enabled"` + Image interface{} `yaml:"image"` + Join interface{} `yaml:"join"` + DataDirectoryHostPath interface{} `yaml:"dataDirectoryHostPath"` + Grpc bool `yaml:"grpc"` + NodeMeta NodeMeta `yaml:"nodeMeta"` + ExposeGossipPorts bool `yaml:"exposeGossipPorts"` + ServiceAccount ServiceAccount `yaml:"serviceAccount"` + Resources Resources `yaml:"resources"` + SecurityContext SecurityContext `yaml:"securityContext"` + ContainerSecurityContext ClientContainerSecurityContext `yaml:"containerSecurityContext"` + ExtraConfig string `yaml:"extraConfig"` + ExtraVolumes []interface{} `yaml:"extraVolumes"` + ExtraContainers []interface{} `yaml:"extraContainers"` + Tolerations string `yaml:"tolerations"` + NodeSelector interface{} `yaml:"nodeSelector"` + Affinity interface{} `yaml:"affinity"` + PriorityClassName string `yaml:"priorityClassName"` + Annotations interface{} `yaml:"annotations"` + ExtraLabels interface{} `yaml:"extraLabels"` + ExtraEnvironmentVars ExtraEnvironmentVars `yaml:"extraEnvironmentVars"` + DNSPolicy interface{} `yaml:"dnsPolicy"` + HostNetwork bool `yaml:"hostNetwork"` + UpdateStrategy interface{} `yaml:"updateStrategy"` + SnapshotAgent SnapshotAgent `yaml:"snapshotAgent"` +} + +type DNS struct { + Enabled string `yaml:"enabled"` + EnableRedirection bool `yaml:"enableRedirection"` + Type string `yaml:"type"` + ClusterIP interface{} `yaml:"clusterIP"` + Annotations interface{} `yaml:"annotations"` + AdditionalSpec interface{} `yaml:"additionalSpec"` +} + +type Port struct { + HTTP int `yaml:"http"` + HTTPS int `yaml:"https"` +} + +type ServiceNodePort struct { + HTTP interface{} `yaml:"http"` + HTTPS interface{} `yaml:"https"` +} + +type UIService struct { + Enabled bool `yaml:"enabled"` + Type interface{} `yaml:"type"` + Port Port `yaml:"port"` + NodePort ServiceNodePort `yaml:"nodePort"` + Annotations interface{} `yaml:"annotations"` + AdditionalSpec interface{} `yaml:"additionalSpec"` +} +type Ingress struct { + Enabled bool `yaml:"enabled"` + IngressClassName string `yaml:"ingressClassName"` + PathType string `yaml:"pathType"` + Hosts []interface{} `yaml:"hosts"` + TLS []interface{} `yaml:"tls"` + Annotations interface{} `yaml:"annotations"` +} + +type UIMetrics struct { + Enabled string `yaml:"enabled"` + Provider string `yaml:"provider"` + BaseURL string `yaml:"baseURL"` +} + +type DashboardURLTemplates struct { + Service string `yaml:"service"` +} + +type UI struct { + Enabled string `yaml:"enabled"` + Service UIService `yaml:"service"` + Ingress Ingress `yaml:"ingress"` + Metrics UIMetrics `yaml:"metrics"` + DashboardURLTemplates DashboardURLTemplates `yaml:"dashboardURLTemplates"` +} + +type ConsulNamespaces struct { + ConsulDestinationNamespace string `yaml:"consulDestinationNamespace"` + MirroringK8S bool `yaml:"mirroringK8S"` + MirroringK8SPrefix string `yaml:"mirroringK8SPrefix"` +} + +type ACLSyncToken struct { + SecretName interface{} `yaml:"secretName"` + SecretKey interface{} `yaml:"secretKey"` +} + +type SyncCatalog struct { + Enabled bool `yaml:"enabled"` + Image interface{} `yaml:"image"` + Default bool `yaml:"default"` + PriorityClassName string `yaml:"priorityClassName"` + ToConsul bool `yaml:"toConsul"` + ToK8S bool `yaml:"toK8S"` + K8SPrefix interface{} `yaml:"k8sPrefix"` + K8SAllowNamespaces []string `yaml:"k8sAllowNamespaces"` + K8SDenyNamespaces []string `yaml:"k8sDenyNamespaces"` + K8SSourceNamespace interface{} `yaml:"k8sSourceNamespace"` + ConsulNamespaces ConsulNamespaces `yaml:"consulNamespaces"` + AddK8SNamespaceSuffix bool `yaml:"addK8SNamespaceSuffix"` + ConsulPrefix interface{} `yaml:"consulPrefix"` + K8STag interface{} `yaml:"k8sTag"` + ConsulNodeName string `yaml:"consulNodeName"` + SyncClusterIPServices bool `yaml:"syncClusterIPServices"` + NodePortSyncType string `yaml:"nodePortSyncType"` + ACLSyncToken ACLSyncToken `yaml:"aclSyncToken"` + NodeSelector interface{} `yaml:"nodeSelector"` + Affinity interface{} `yaml:"affinity"` + Tolerations interface{} `yaml:"tolerations"` + ServiceAccount ServiceAccount `yaml:"serviceAccount"` + Resources Resources `yaml:"resources"` + LogLevel string `yaml:"logLevel"` + ConsulWriteInterval interface{} `yaml:"consulWriteInterval"` + ExtraLabels interface{} `yaml:"extraLabels"` +} + +type TransparentProxy struct { + DefaultEnabled bool `yaml:"defaultEnabled"` + DefaultOverwriteProbes bool `yaml:"defaultOverwriteProbes"` +} + +type Metrics struct { + DefaultEnabled string `yaml:"defaultEnabled"` + DefaultEnableMerging bool `yaml:"defaultEnableMerging"` + DefaultMergedMetricsPort int `yaml:"defaultMergedMetricsPort"` + DefaultPrometheusScrapePort int `yaml:"defaultPrometheusScrapePort"` + DefaultPrometheusScrapePath string `yaml:"defaultPrometheusScrapePath"` +} + +type ACLInjectToken struct { + SecretName interface{} `yaml:"secretName"` + SecretKey interface{} `yaml:"secretKey"` +} + +type SidecarProxy struct { + Resources Resources `yaml:"resources"` +} + +type InitContainer struct { + Resources Resources `yaml:"resources"` +} + +type ConnectInject struct { + Enabled bool `yaml:"enabled"` + Replicas int `yaml:"replicas"` + Image interface{} `yaml:"image"` + Default bool `yaml:"default"` + TransparentProxy TransparentProxy `yaml:"transparentProxy"` + Metrics Metrics `yaml:"metrics"` + EnvoyExtraArgs interface{} `yaml:"envoyExtraArgs"` + PriorityClassName string `yaml:"priorityClassName"` + ImageConsul interface{} `yaml:"imageConsul"` + LogLevel string `yaml:"logLevel"` + ServiceAccount ServiceAccount `yaml:"serviceAccount"` + Resources Resources `yaml:"resources"` + FailurePolicy string `yaml:"failurePolicy"` + NamespaceSelector string `yaml:"namespaceSelector"` + K8SAllowNamespaces []string `yaml:"k8sAllowNamespaces"` + K8SDenyNamespaces []interface{} `yaml:"k8sDenyNamespaces"` + ConsulNamespaces ConsulNamespaces `yaml:"consulNamespaces"` + NodeSelector interface{} `yaml:"nodeSelector"` + Affinity interface{} `yaml:"affinity"` + Tolerations interface{} `yaml:"tolerations"` + ACLBindingRuleSelector string `yaml:"aclBindingRuleSelector"` + OverrideAuthMethodName string `yaml:"overrideAuthMethodName"` + ACLInjectToken ACLInjectToken `yaml:"aclInjectToken"` + SidecarProxy SidecarProxy `yaml:"sidecarProxy"` + InitContainer InitContainer `yaml:"initContainer"` +} + +type ACLToken struct { + SecretName interface{} `yaml:"secretName"` + SecretKey interface{} `yaml:"secretKey"` +} + +type Controller struct { + Enabled bool `yaml:"enabled"` + Replicas int `yaml:"replicas"` + LogLevel string `yaml:"logLevel"` + ServiceAccount ServiceAccount `yaml:"serviceAccount"` + Resources Resources `yaml:"resources"` + NodeSelector interface{} `yaml:"nodeSelector"` + Tolerations interface{} `yaml:"tolerations"` + Affinity interface{} `yaml:"affinity"` + PriorityClassName string `yaml:"priorityClassName"` + ACLToken ACLToken `yaml:"aclToken"` +} + +type WanAddress struct { + Source string `yaml:"source"` + Port int `yaml:"port"` + Static string `yaml:"static"` +} + +type InitCopyConsulContainer struct { + Resources Resources `yaml:"resources"` +} + +type InitServiceInitContainer struct { + Resources Resources `yaml:"resources"` +} + +type MeshGateway struct { + Enabled bool `yaml:"enabled"` + Replicas int `yaml:"replicas"` + WanAddress WanAddress `yaml:"wanAddress"` + Service Service `yaml:"service"` + HostNetwork bool `yaml:"hostNetwork"` + DNSPolicy interface{} `yaml:"dnsPolicy"` + ConsulServiceName string `yaml:"consulServiceName"` + ContainerPort int `yaml:"containerPort"` + HostPort interface{} `yaml:"hostPort"` + ServiceAccount ServiceAccount `yaml:"serviceAccount"` + Resources Resources `yaml:"resources"` + InitCopyConsulContainer InitCopyConsulContainer `yaml:"initCopyConsulContainer"` + InitServiceInitContainer InitServiceInitContainer `yaml:"initServiceInitContainer"` + Affinity string `yaml:"affinity"` + Tolerations interface{} `yaml:"tolerations"` + NodeSelector interface{} `yaml:"nodeSelector"` + PriorityClassName string `yaml:"priorityClassName"` + Annotations interface{} `yaml:"annotations"` +} + +type ServicePorts struct { + Port int `yaml:"port"` + NodePort interface{} `yaml:"nodePort"` +} + +type DefaultsService struct { + Type string `yaml:"type"` + Ports []ServicePorts `yaml:"ports"` + Annotations interface{} `yaml:"annotations"` + AdditionalSpec interface{} `yaml:"additionalSpec"` +} + +type IngressGatewayDefaults struct { + Replicas int `yaml:"replicas"` + Service DefaultsService `yaml:"service"` + ServiceAccount ServiceAccount `yaml:"serviceAccount"` + Resources Resources `yaml:"resources"` + InitCopyConsulContainer InitCopyConsulContainer `yaml:"initCopyConsulContainer"` + Affinity string `yaml:"affinity"` + Tolerations interface{} `yaml:"tolerations"` + NodeSelector interface{} `yaml:"nodeSelector"` + PriorityClassName string `yaml:"priorityClassName"` + TerminationGracePeriodSeconds int `yaml:"terminationGracePeriodSeconds"` + Annotations interface{} `yaml:"annotations"` + ConsulNamespace string `yaml:"consulNamespace"` +} + +type Gateways struct { + Name string `yaml:"name"` +} + +type IngressGateways struct { + Enabled bool `yaml:"enabled"` + Defaults IngressGatewayDefaults `yaml:"defaults"` + Gateways []Gateways `yaml:"gateways"` +} + +type Defaults struct { + Replicas int `yaml:"replicas"` + ExtraVolumes []interface{} `yaml:"extraVolumes"` + Resources Resources `yaml:"resources"` + InitCopyConsulContainer InitCopyConsulContainer `yaml:"initCopyConsulContainer"` + Affinity string `yaml:"affinity"` + Tolerations interface{} `yaml:"tolerations"` + NodeSelector interface{} `yaml:"nodeSelector"` + PriorityClassName string `yaml:"priorityClassName"` + Annotations interface{} `yaml:"annotations"` + ServiceAccount ServiceAccount `yaml:"serviceAccount"` + ConsulNamespace string `yaml:"consulNamespace"` +} + +type TerminatingGateways struct { + Enabled bool `yaml:"enabled"` + Defaults Defaults `yaml:"defaults"` + Gateways []Gateways `yaml:"gateways"` +} + +type CopyAnnotations struct { + Service interface{} `yaml:"service"` +} + +type ManagedGatewayClass struct { + Enabled bool `yaml:"enabled"` + NodeSelector interface{} `yaml:"nodeSelector"` + ServiceType string `yaml:"serviceType"` + UseHostPorts bool `yaml:"useHostPorts"` + CopyAnnotations CopyAnnotations `yaml:"copyAnnotations"` +} + +type Service struct { + Annotations interface{} `yaml:"annotations"` +} + +type APIGatewayController struct { + Replicas int `yaml:"replicas"` + Annotations interface{} `yaml:"annotations"` + PriorityClassName string `yaml:"priorityClassName"` + NodeSelector interface{} `yaml:"nodeSelector"` + Service Service `yaml:"service"` +} + +type APIGateway struct { + Enabled bool `yaml:"enabled"` + Image interface{} `yaml:"image"` + LogLevel string `yaml:"logLevel"` + ManagedGatewayClass ManagedGatewayClass `yaml:"managedGatewayClass"` + ConsulNamespaces ConsulNamespaces `yaml:"consulNamespaces"` + ServiceAccount ServiceAccount `yaml:"serviceAccount"` + Controller APIGatewayController `yaml:"controller"` +} + +type WebhookCertManager struct { + Tolerations interface{} `yaml:"tolerations"` +} + +type Prometheus struct { + Enabled bool `yaml:"enabled"` +} + +type Tests struct { + Enabled bool `yaml:"enabled"` +} diff --git a/cli/release/release.go b/cli/release/release.go new file mode 100644 index 0000000000..47749d28a1 --- /dev/null +++ b/cli/release/release.go @@ -0,0 +1,25 @@ +package release + +import ( + "github.com/hashicorp/consul-k8s/cli/helm" +) + +// Release represents a Consul cluster and its associated configuration. +type Release struct { + // Name is the name of the release. + Name string + + // Namespace is the Kubernetes namespace in which the release is deployed. + Namespace string + + // Configuration is the Helm configuration for the release. + Configuration helm.Values +} + +// ShouldExpectFederationSecret returns true if the non-primary DC in a +// federated cluster. +func (r *Release) ShouldExpectFederationSecret() bool { + return r.Configuration.Global.Federation.Enabled && + r.Configuration.Global.Datacenter != r.Configuration.Global.Federation.PrimaryDatacenter && + !r.Configuration.Global.Federation.CreateFederationSecret +} diff --git a/cli/release/release_test.go b/cli/release/release_test.go new file mode 100644 index 0000000000..ed6b39329c --- /dev/null +++ b/cli/release/release_test.go @@ -0,0 +1,63 @@ +package release + +import ( + "testing" + + "github.com/hashicorp/consul-k8s/cli/helm" + "github.com/stretchr/testify/require" +) + +func TestShouldExpectFederationSecret(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + configuration helm.Values + expected bool + }{ + "Primary DC, no federation": { + configuration: helm.Values{ + Global: helm.Global{ + Datacenter: "dc1", + }, + }, + expected: false, + }, + "Primary DC, federation enabled": { + configuration: helm.Values{ + + Global: helm.Global{ + Datacenter: "dc1", + Federation: helm.Federation{ + Enabled: true, + PrimaryDatacenter: "dc1", + }, + }, + }, + expected: false, + }, + "Non-primary DC, federation enabled": { + configuration: helm.Values{ + Global: helm.Global{ + Datacenter: "dc2", + Federation: helm.Federation{ + Enabled: true, + PrimaryDatacenter: "dc1", + CreateFederationSecret: false, + }, + }, + }, + expected: true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + release := Release{ + Configuration: tc.configuration, + } + + actual := release.ShouldExpectFederationSecret() + require.Equal(t, tc.expected, actual) + }) + } +} diff --git a/cli/validation/kubernetes.go b/cli/validation/kubernetes.go new file mode 100644 index 0000000000..8ece89efc5 --- /dev/null +++ b/cli/validation/kubernetes.go @@ -0,0 +1,20 @@ +package validation + +import ( + "context" + "fmt" + + "github.com/hashicorp/consul-k8s/cli/common" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// ListConsulSecrets attempts to find secrets with the Consul label. +func ListConsulSecrets(ctx context.Context, client kubernetes.Interface) (*v1.SecretList, error) { + secrets, err := client.CoreV1().Secrets("").List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", common.CLILabelKey, common.CLILabelValue), + }) + + return secrets, err +} diff --git a/cli/validation/kubernetes_test.go b/cli/validation/kubernetes_test.go new file mode 100644 index 0000000000..a17c066719 --- /dev/null +++ b/cli/validation/kubernetes_test.go @@ -0,0 +1,72 @@ +package validation + +import ( + "context" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestListConsulSecrets(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + secrets *v1.SecretList + expectedSecrets int + }{ + "No secrets": { + secrets: &v1.SecretList{}, + expectedSecrets: 0, + }, + "A Consul Secret": { + secrets: &v1.SecretList{ + Items: []v1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-consul-bootstrap-acl-token", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + }, + }, + }, + expectedSecrets: 1, + }, + "A Consul and a non-Consul Secret": { + secrets: &v1.SecretList{ + Items: []v1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-consul-bootstrap-acl-token", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "not-a-consul-secret", + }, + }, + }, + }, + expectedSecrets: 1, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + client := fake.NewSimpleClientset() + + for _, secret := range tc.secrets.Items { + _, err := client.CoreV1().Secrets(v1.NamespaceDefault).Create(context.Background(), &secret, metav1.CreateOptions{}) + require.NoError(t, err) + } + + actual, err := ListConsulSecrets(context.Background(), client) + require.NoError(t, err) + require.Equal(t, tc.expectedSecrets, len(actual.Items)) + }) + } +}