diff --git a/.golangci.yml b/.golangci.yml index fcb12ed72..08f419a65 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -60,3 +60,6 @@ issues: - path: pkg/scripts text: "`registry` always receives `\"127.0.0.1:5000\"`" + + - path: pkg/credentials + text: "cyclomatic complexity 32 of func `openstackValidationFunc` is high" diff --git a/pkg/credentials/credentials.go b/pkg/credentials/credentials.go index dc7609e9d..2a7cd1853 100644 --- a/pkg/credentials/credentials.go +++ b/pkg/credentials/credentials.go @@ -31,27 +31,29 @@ import ( // The environment variable names with credential in them const ( // Variables that KubeOne (and Terraform) expect to see - AWSAccessKeyID = "AWS_ACCESS_KEY_ID" - AWSSecretAccessKey = "AWS_SECRET_ACCESS_KEY" //nolint:gosec - AzureClientID = "ARM_CLIENT_ID" - AzureClientSecret = "ARM_CLIENT_SECRET" //nolint:gosec - AzureTenantID = "ARM_TENANT_ID" - AzureSubscribtionID = "ARM_SUBSCRIPTION_ID" - DigitalOceanTokenKey = "DIGITALOCEAN_TOKEN" - GoogleServiceAccountKey = "GOOGLE_CREDENTIALS" - HetznerTokenKey = "HCLOUD_TOKEN" - OpenStackAuthURL = "OS_AUTH_URL" - OpenStackDomainName = "OS_DOMAIN_NAME" - OpenStackPassword = "OS_PASSWORD" - OpenStackRegionName = "OS_REGION_NAME" - OpenStackTenantID = "OS_TENANT_ID" - OpenStackTenantName = "OS_TENANT_NAME" - OpenStackUserName = "OS_USERNAME" - PacketAPIKey = "PACKET_AUTH_TOKEN" //nolint:gosec - PacketProjectID = "PACKET_PROJECT_ID" - VSphereAddress = "VSPHERE_SERVER" - VSpherePassword = "VSPHERE_PASSWORD" - VSphereUsername = "VSPHERE_USER" + AWSAccessKeyID = "AWS_ACCESS_KEY_ID" + AWSSecretAccessKey = "AWS_SECRET_ACCESS_KEY" //nolint:gosec + AzureClientID = "ARM_CLIENT_ID" + AzureClientSecret = "ARM_CLIENT_SECRET" //nolint:gosec + AzureTenantID = "ARM_TENANT_ID" + AzureSubscribtionID = "ARM_SUBSCRIPTION_ID" + DigitalOceanTokenKey = "DIGITALOCEAN_TOKEN" + GoogleServiceAccountKey = "GOOGLE_CREDENTIALS" + HetznerTokenKey = "HCLOUD_TOKEN" + OpenStackAuthURL = "OS_AUTH_URL" + OpenStackDomainName = "OS_DOMAIN_NAME" + OpenStackPassword = "OS_PASSWORD" + OpenStackRegionName = "OS_REGION_NAME" + OpenStackTenantID = "OS_TENANT_ID" + OpenStackTenantName = "OS_TENANT_NAME" + OpenStackUserName = "OS_USERNAME" + OpenStackApplicationCredentialID = "OS_APPLICATION_CREDENTIAL_ID" + OpenStackApplicationCredentialSecret = "OS_APPLICATION_CREDENTIAL_SECRET" + PacketAPIKey = "PACKET_AUTH_TOKEN" //nolint:gosec + PacketProjectID = "PACKET_PROJECT_ID" + VSphereAddress = "VSPHERE_SERVER" + VSpherePassword = "VSPHERE_PASSWORD" + VSphereUsername = "VSPHERE_USER" // Variables that machine-controller expects AzureClientIDMC = "AZURE_CLIENT_ID" @@ -158,6 +160,8 @@ func ProviderCredentials(cloudProvider kubeone.CloudProviderSpec, credentialsFil {Name: OpenStackAuthURL}, {Name: OpenStackUserName, MachineControllerName: OpenStackUserNameMC}, {Name: OpenStackPassword}, + {Name: OpenStackApplicationCredentialID}, + {Name: OpenStackApplicationCredentialSecret}, {Name: OpenStackDomainName}, {Name: OpenStackRegionName}, {Name: OpenStackTenantID}, @@ -283,15 +287,57 @@ func defaultValidationFunc(creds map[string]string) error { } func openstackValidationFunc(creds map[string]string) error { - for k, v := range creds { - if k == OpenStackTenantID || k == OpenStackTenantName { - continue - } - if len(v) == 0 { - return errors.Errorf("key %v is required but isn't present", k) + alwaysRequired := []string{OpenStackAuthURL, OpenStackDomainName, OpenStackRegionName} + + for _, key := range alwaysRequired { + if v, ok := creds[key]; !ok || len(v) == 0 { + return errors.Errorf("key %v is required but is not present", key) } } + var ( + appCredsIDOkay bool + appCredsSecretOkay bool + userCredsUsernameOkay bool + userCredsPasswordOkay bool + ) + + if v, ok := creds[OpenStackApplicationCredentialID]; ok && len(v) != 0 { + appCredsIDOkay = true + } + + if v, ok := creds[OpenStackApplicationCredentialSecret]; ok && len(v) != 0 { + appCredsSecretOkay = true + } + + if v, ok := creds[OpenStackUserName]; ok && len(v) != 0 { + userCredsUsernameOkay = true + } + + if v, ok := creds[OpenStackPassword]; ok && len(v) != 0 { + userCredsPasswordOkay = true + } + + if (appCredsIDOkay || appCredsSecretOkay) && (userCredsUsernameOkay || userCredsPasswordOkay) { + return errors.Errorf("both app credentials (%s %s) and user credentials (%s %s) found", + OpenStackApplicationCredentialID, OpenStackApplicationCredentialSecret, + OpenStackUserName, OpenStackPassword) + } + + if (appCredsIDOkay && !appCredsSecretOkay) || (!appCredsIDOkay && appCredsSecretOkay) { + return errors.Errorf("only one of %s, %s is set for application credentials", + OpenStackApplicationCredentialID, OpenStackApplicationCredentialSecret) + } + + if (userCredsUsernameOkay && !userCredsPasswordOkay) || (!userCredsUsernameOkay && userCredsPasswordOkay) { + return errors.Errorf("only one of %s, %s is set for user credentials", + OpenStackUserName, OpenStackPassword) + } + + if (!appCredsIDOkay && !appCredsSecretOkay) && (!userCredsUsernameOkay && !userCredsPasswordOkay) { + return errors.New("no valid credentials (either application or user) found") + } + if v, ok := creds[OpenStackTenantID]; !ok || len(v) == 0 { if v, ok := creds[OpenStackTenantName]; !ok || len(v) == 0 { return errors.Errorf("key %v or %v is required but isn't present", OpenStackTenantID, OpenStackTenantName) diff --git a/pkg/credentials/credentials_test.go b/pkg/credentials/credentials_test.go new file mode 100644 index 000000000..b01e3cdf6 --- /dev/null +++ b/pkg/credentials/credentials_test.go @@ -0,0 +1,157 @@ +/* +Copyright 2021 The KubeOne Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package credentials + +import ( + "errors" + "testing" +) + +func TestOpenstackValidationFunc(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + creds map[string]string + err error + }{ + { + name: "no-credentials", + creds: map[string]string{}, + err: errors.New("key OS_AUTH_URL is required but is not present"), + }, + { + name: "application-credentials", + creds: map[string]string{ + "OS_TENANT_NAME": "test", + "OS_AUTH_URL": "https://localhost:5000", + "OS_DOMAIN_NAME": "test", + "OS_REGION_NAME": "de", + "OS_APPLICATION_CREDENTIAL_ID": "1234", + "OS_APPLICATION_CREDENTIAL_SECRET": "5678", + }, + err: nil, + }, + { + name: "no-credential-secret", + creds: map[string]string{ + "OS_TENANT_NAME": "test", + "OS_AUTH_URL": "https://localhost:5000", + "OS_DOMAIN_NAME": "test", + "OS_REGION_NAME": "de", + "OS_APPLICATION_CREDENTIAL_ID": "1234", + }, + err: errors.New("only one of OS_APPLICATION_CREDENTIAL_ID, OS_APPLICATION_CREDENTIAL_SECRET is set for application credentials"), + }, + { + name: "no-credential-id", + creds: map[string]string{ + "OS_TENANT_NAME": "test", + "OS_AUTH_URL": "https://localhost:5000", + "OS_DOMAIN_NAME": "test", + "OS_REGION_NAME": "de", + "OS_APPLICATION_CREDENTIAL_SECRET": "5678", + }, + err: errors.New("only one of OS_APPLICATION_CREDENTIAL_ID, OS_APPLICATION_CREDENTIAL_SECRET is set for application credentials"), + }, + { + name: "user-credentials", + creds: map[string]string{ + "OS_TENANT_NAME": "test", + "OS_AUTH_URL": "https://localhost:5000", + "OS_DOMAIN_NAME": "test", + "OS_REGION_NAME": "de", + "OS_USERNAME": "1234", + "OS_PASSWORD": "5678", + }, + err: nil, + }, + { + name: "no-password", + creds: map[string]string{ + "OS_TENANT_NAME": "test", + "OS_AUTH_URL": "https://localhost:5000", + "OS_DOMAIN_NAME": "test", + "OS_REGION_NAME": "de", + "OS_USERNAME": "1234", + }, + err: errors.New("only one of OS_USERNAME, OS_PASSWORD is set for user credentials"), + }, + { + name: "no-username", + creds: map[string]string{ + "OS_TENANT_NAME": "test", + "OS_AUTH_URL": "https://localhost:5000", + "OS_DOMAIN_NAME": "test", + "OS_REGION_NAME": "de", + "OS_PASSWORD": "5678", + }, + err: errors.New("only one of OS_USERNAME, OS_PASSWORD is set for user credentials"), + }, + { + name: "mixed-credentials-1", + creds: map[string]string{ + "OS_TENANT_NAME": "test", + "OS_AUTH_URL": "https://localhost:5000", + "OS_DOMAIN_NAME": "test", + "OS_REGION_NAME": "de", + "OS_APPLICATION_CREDENTIAL_ID": "1234", + "OS_PASSWORD": "5678", + }, + err: errors.New("both app credentials (OS_APPLICATION_CREDENTIAL_ID OS_APPLICATION_CREDENTIAL_SECRET) and user credentials (OS_USERNAME OS_PASSWORD) found"), + }, + { + name: "mixed-credentials-2", + creds: map[string]string{ + "OS_TENANT_NAME": "test", + "OS_AUTH_URL": "https://localhost:5000", + "OS_DOMAIN_NAME": "test", + "OS_REGION_NAME": "de", + "OS_APPLICATION_CREDENTIAL_SECRET": "5678", + "OS_USERNAME": "1234", + }, + err: errors.New("both app credentials (OS_APPLICATION_CREDENTIAL_ID OS_APPLICATION_CREDENTIAL_SECRET) and user credentials (OS_USERNAME OS_PASSWORD) found"), + }, + { + name: "mixed-credentials-3", + creds: map[string]string{ + "OS_TENANT_NAME": "test", + "OS_AUTH_URL": "https://localhost:5000", + "OS_DOMAIN_NAME": "test", + "OS_REGION_NAME": "de", + "OS_APPLICATION_CREDENTIAL_ID": "1234", + "OS_APPLICATION_CREDENTIAL_SECRET": "5678", + "OS_USERNAME": "1234", + "OS_PASSWORD": "5678", + }, + err: errors.New("both app credentials (OS_APPLICATION_CREDENTIAL_ID OS_APPLICATION_CREDENTIAL_SECRET) and user credentials (OS_USERNAME OS_PASSWORD) found"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := openstackValidationFunc(tt.creds) + if tt.err != nil && err != nil { + if err.Error() != tt.err.Error() { + t.Errorf("expected error = '%v', got error = '%v'", tt.err.Error(), err.Error()) + } + } else if err != tt.err { + t.Errorf("%s: expected error = %v, got error = %v", tt.name, tt.err, err) + } + }) + } +}