// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package concierge import ( "context" "os" "testing" "github.com/stretchr/testify/require" "k8s.io/utils/ptr" "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/plog" ) func TestFromPath(t *testing.T) { tests := []struct { name string yaml string wantConfig *Config wantError string }{ { name: "Fully filled out", yaml: here.Doc(` --- discovery: url: https://some.discovery/url api: servingCertificate: durationSeconds: 3600 renewBeforeSeconds: 2400 apiGroupSuffix: some.suffix.com aggregatedAPIServerPort: 12345 impersonationProxyServerPort: 4242 names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api kubeCertAgentPrefix: kube-cert-agent-prefix impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value extraName: extraName-value labels: myLabelKey1: myLabelValue1 myLabelKey2: myLabelValue2 kubeCertAgent: namePrefix: kube-cert-agent-name-prefix- image: kube-cert-agent-image imagePullSecrets: [kube-cert-agent-image-pull-secret] logLevel: debug `), wantConfig: &Config{ DiscoveryInfo: DiscoveryInfoSpec{ URL: ptr.To("https://some.discovery/url"), }, APIConfig: APIConfigSpec{ ServingCertificateConfig: ServingCertificateConfigSpec{ DurationSeconds: ptr.To[int64](3600), RenewBeforeSeconds: ptr.To[int64](2400), }, }, APIGroupSuffix: ptr.To("some.suffix.com"), AggregatedAPIServerPort: ptr.To[int64](12345), ImpersonationProxyServerPort: ptr.To[int64](4242), NamesConfig: NamesConfigSpec{ ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", CredentialIssuer: "pinniped-config", APIService: "pinniped-api", ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value", ImpersonationClusterIPService: "impersonationClusterIPService-value", ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value", ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value", ImpersonationSignerSecret: "impersonationSignerSecret-value", AgentServiceAccount: "agentServiceAccount-value", }, Labels: map[string]string{ "myLabelKey1": "myLabelValue1", "myLabelKey2": "myLabelValue2", }, KubeCertAgentConfig: KubeCertAgentSpec{ NamePrefix: ptr.To("kube-cert-agent-name-prefix-"), Image: ptr.To("kube-cert-agent-image"), ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, }, LogLevel: func(level plog.LogLevel) *plog.LogLevel { return &level }(plog.LevelDebug), Log: plog.LogSpec{ Level: plog.LevelDebug, }, }, }, { name: "Fully filled out new log struct", yaml: here.Doc(` --- discovery: url: https://some.discovery/url api: servingCertificate: durationSeconds: 3600 renewBeforeSeconds: 2400 apiGroupSuffix: some.suffix.com aggregatedAPIServerPort: 12345 impersonationProxyServerPort: 4242 names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api kubeCertAgentPrefix: kube-cert-agent-prefix impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value extraName: extraName-value labels: myLabelKey1: myLabelValue1 myLabelKey2: myLabelValue2 kubeCertAgent: namePrefix: kube-cert-agent-name-prefix- image: kube-cert-agent-image imagePullSecrets: [kube-cert-agent-image-pull-secret] log: level: all format: json `), wantConfig: &Config{ DiscoveryInfo: DiscoveryInfoSpec{ URL: ptr.To("https://some.discovery/url"), }, APIConfig: APIConfigSpec{ ServingCertificateConfig: ServingCertificateConfigSpec{ DurationSeconds: ptr.To[int64](3600), RenewBeforeSeconds: ptr.To[int64](2400), }, }, APIGroupSuffix: ptr.To("some.suffix.com"), AggregatedAPIServerPort: ptr.To[int64](12345), ImpersonationProxyServerPort: ptr.To[int64](4242), NamesConfig: NamesConfigSpec{ ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", CredentialIssuer: "pinniped-config", APIService: "pinniped-api", ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value", ImpersonationClusterIPService: "impersonationClusterIPService-value", ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value", ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value", ImpersonationSignerSecret: "impersonationSignerSecret-value", AgentServiceAccount: "agentServiceAccount-value", }, Labels: map[string]string{ "myLabelKey1": "myLabelValue1", "myLabelKey2": "myLabelValue2", }, KubeCertAgentConfig: KubeCertAgentSpec{ NamePrefix: ptr.To("kube-cert-agent-name-prefix-"), Image: ptr.To("kube-cert-agent-image"), ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, }, Log: plog.LogSpec{ Level: plog.LevelAll, Format: plog.FormatJSON, }, }, }, { name: "Fully filled out old log and new log struct", yaml: here.Doc(` --- discovery: url: https://some.discovery/url api: servingCertificate: durationSeconds: 3600 renewBeforeSeconds: 2400 apiGroupSuffix: some.suffix.com aggregatedAPIServerPort: 12345 impersonationProxyServerPort: 4242 names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api kubeCertAgentPrefix: kube-cert-agent-prefix impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value extraName: extraName-value labels: myLabelKey1: myLabelValue1 myLabelKey2: myLabelValue2 kubeCertAgent: namePrefix: kube-cert-agent-name-prefix- image: kube-cert-agent-image imagePullSecrets: [kube-cert-agent-image-pull-secret] logLevel: debug log: level: all format: json `), wantConfig: &Config{ DiscoveryInfo: DiscoveryInfoSpec{ URL: ptr.To("https://some.discovery/url"), }, APIConfig: APIConfigSpec{ ServingCertificateConfig: ServingCertificateConfigSpec{ DurationSeconds: ptr.To[int64](3600), RenewBeforeSeconds: ptr.To[int64](2400), }, }, APIGroupSuffix: ptr.To("some.suffix.com"), AggregatedAPIServerPort: ptr.To[int64](12345), ImpersonationProxyServerPort: ptr.To[int64](4242), NamesConfig: NamesConfigSpec{ ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", CredentialIssuer: "pinniped-config", APIService: "pinniped-api", ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value", ImpersonationClusterIPService: "impersonationClusterIPService-value", ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value", ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value", ImpersonationSignerSecret: "impersonationSignerSecret-value", AgentServiceAccount: "agentServiceAccount-value", }, Labels: map[string]string{ "myLabelKey1": "myLabelValue1", "myLabelKey2": "myLabelValue2", }, KubeCertAgentConfig: KubeCertAgentSpec{ NamePrefix: ptr.To("kube-cert-agent-name-prefix-"), Image: ptr.To("kube-cert-agent-image"), ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, }, LogLevel: func(level plog.LogLevel) *plog.LogLevel { return &level }(plog.LevelDebug), Log: plog.LogSpec{ Level: plog.LevelDebug, Format: plog.FormatJSON, }, }, }, { name: "invalid log format", yaml: here.Doc(` --- names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value log: level: all format: snorlax `), wantError: "decode yaml: error unmarshaling JSON: while decoding JSON: invalid log format, valid choices are the empty string, json and text", }, { name: "When only the required fields are present, causes other fields to be defaulted", yaml: here.Doc(` --- names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value `), wantConfig: &Config{ DiscoveryInfo: DiscoveryInfoSpec{ URL: nil, }, APIGroupSuffix: ptr.To("pinniped.dev"), AggregatedAPIServerPort: ptr.To[int64](10250), ImpersonationProxyServerPort: ptr.To[int64](8444), APIConfig: APIConfigSpec{ ServingCertificateConfig: ServingCertificateConfigSpec{ DurationSeconds: ptr.To[int64](60 * 60 * 24 * 365), // about a year RenewBeforeSeconds: ptr.To[int64](60 * 60 * 24 * 30 * 9), // about 9 months }, }, NamesConfig: NamesConfigSpec{ ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", CredentialIssuer: "pinniped-config", APIService: "pinniped-api", ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value", ImpersonationClusterIPService: "impersonationClusterIPService-value", ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value", ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value", ImpersonationSignerSecret: "impersonationSignerSecret-value", AgentServiceAccount: "agentServiceAccount-value", }, Labels: map[string]string{}, KubeCertAgentConfig: KubeCertAgentSpec{ NamePrefix: ptr.To("pinniped-kube-cert-agent-"), Image: ptr.To("debian:latest"), }, }, }, { name: "Empty", yaml: here.Doc(``), wantError: "validate names: missing required names: servingCertificateSecret, credentialIssuer, " + "apiService, impersonationLoadBalancerService, " + "impersonationClusterIPService, impersonationTLSCertificateSecret, impersonationCACertificateSecret, " + "impersonationSignerSecret, agentServiceAccount", }, { name: "Missing apiService name", yaml: here.Doc(` --- names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value `), wantError: "validate names: missing required names: apiService", }, { name: "Missing credentialIssuer name", yaml: here.Doc(` --- names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value `), wantError: "validate names: missing required names: credentialIssuer", }, { name: "Missing servingCertificateSecret name", yaml: here.Doc(` --- names: credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value `), wantError: "validate names: missing required names: servingCertificateSecret", }, { name: "Missing impersonationLoadBalancerService name", yaml: here.Doc(` --- names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value `), wantError: "validate names: missing required names: impersonationLoadBalancerService", }, { name: "Missing impersonationClusterIPService name", yaml: here.Doc(` --- names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value `), wantError: "validate names: missing required names: impersonationClusterIPService", }, { name: "Missing impersonationTLSCertificateSecret name", yaml: here.Doc(` --- names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value `), wantError: "validate names: missing required names: impersonationTLSCertificateSecret", }, { name: "Missing impersonationCACertificateSecret name", yaml: here.Doc(` --- names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value `), wantError: "validate names: missing required names: impersonationCACertificateSecret", }, { name: "Missing impersonationSignerSecret name", yaml: here.Doc(` --- names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value agentServiceAccount: agentServiceAccount-value `), wantError: "validate names: missing required names: impersonationSignerSecret", }, { name: "Missing several required names", yaml: here.Doc(` --- names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationClusterIPService: impersonationClusterIPService-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value `), wantError: "validate names: missing required names: " + "impersonationTLSCertificateSecret, impersonationCACertificateSecret", }, { name: "InvalidDurationRenewBefore", yaml: here.Doc(` --- api: servingCertificate: durationSeconds: 2400 renewBeforeSeconds: 3600 names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value `), wantError: "validate api: durationSeconds cannot be smaller than renewBeforeSeconds", }, { name: "NegativeRenewBefore", yaml: here.Doc(` --- api: servingCertificate: durationSeconds: 2400 renewBeforeSeconds: -10 names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value `), wantError: "validate api: renewBefore must be positive", }, { name: "AggregatedAPIServerPortDefault too small", yaml: here.Doc(` --- aggregatedAPIServerPort: 1023 `), wantError: "validate aggregatedAPIServerPort: must be within range 1024 to 65535", }, { name: "AggregatedAPIServerPortDefault too large", yaml: here.Doc(` --- aggregatedAPIServerPort: 65536 `), wantError: "validate aggregatedAPIServerPort: must be within range 1024 to 65535", }, { name: "ImpersonationProxyServerPort too small", yaml: here.Doc(` --- impersonationProxyServerPort: 1023 `), wantError: "validate impersonationProxyServerPort: must be within range 1024 to 65535", }, { name: "ImpersonationProxyServerPort too large", yaml: here.Doc(` --- impersonationProxyServerPort: 65536 `), wantError: "validate impersonationProxyServerPort: must be within range 1024 to 65535", }, { name: "ZeroRenewBefore", yaml: here.Doc(` --- api: servingCertificate: durationSeconds: 2400 renewBeforeSeconds: 0 names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value `), wantError: "validate api: renewBefore must be positive", }, { name: "InvalidAPIGroupSuffix", yaml: here.Doc(` --- api: servingCertificate: durationSeconds: 3600 renewBeforeSeconds: 2400 apiGroupSuffix: .starts.with.dot names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value `), wantError: "validate apiGroupSuffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')", }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { // this is a serial test because it sets the global logger // Write yaml to temp file f, err := os.CreateTemp("", "pinniped-test-config-yaml-*") require.NoError(t, err) defer func() { err := os.Remove(f.Name()) require.NoError(t, err) }() _, err = f.WriteString(test.yaml) require.NoError(t, err) err = f.Close() require.NoError(t, err) // Test FromPath() ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) config, err := FromPath(ctx, f.Name()) if test.wantError != "" { require.EqualError(t, err, test.wantError) } else { require.NoError(t, err) require.Equal(t, test.wantConfig, config) } }) } }