Skip to content

Commit

Permalink
Implement support for AWSCredentialsOverrideRef
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Shen <mshen@redhat.com>
  • Loading branch information
mjlshen committed Jan 26, 2023
1 parent ddc2dd5 commit 62262f7
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 7 deletions.
8 changes: 7 additions & 1 deletion api/v1alpha2/vpcendpoint_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package v1alpha2

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -86,7 +87,6 @@ type Route53PrivateHostedZone struct {
AutoDiscover bool `json:"autoDiscoverPrivateHostedZone,omitempty"`

// DomainName specifies the domain name of a Route 53 Private Hosted Zone to create
// TODO: Implement
DomainName string `json:"domainName,omitempty"`

// Id specifies the AWS ID of an existing Route 53 Private Hosted Zone to use
Expand Down Expand Up @@ -118,6 +118,12 @@ type VpcEndpointSpec struct {

// +kubebuilder:validation:Optional

// AWSCredentialOverride is a Kubernetes secret containing AWS credentials for the operator to use for reconciling
// this specific vpcendpoint Custom Resource
AWSCredentialOverrideRef *corev1.SecretReference `json:"awsCredentialOverrideRef,omitempty"`

// +kubebuilder:validation:Optional

// Region will allow AVO to create VPC Endpoints and other AWS infrastructure in a specific region
// Defaults to the same region as the cluster AVO is running on
Region string `json:"region,omitempty"`
Expand Down
10 changes: 8 additions & 2 deletions api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions controllers/vpcendpoint/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
avov1alpha2 "github.com/openshift/aws-vpce-operator/api/v1alpha2"
"github.com/openshift/aws-vpce-operator/pkg/aws_client"
"github.com/openshift/aws-vpce-operator/pkg/infrastructures"
"github.com/openshift/aws-vpce-operator/pkg/secrets"
"github.com/openshift/aws-vpce-operator/pkg/util"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -79,12 +80,22 @@ func (r *VpcEndpointReconciler) parseClusterInfo(ctx context.Context, vpce *avov
r.log.V(1).Info("Parsed region from infrastructure", "region", region)
}

if refreshAWSSession {
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(r.clusterInfo.region))
if vpce.Spec.AWSCredentialOverrideRef != nil {
// Use the provided override credentials for this specific vpcendpoint
cfg, err := secrets.ParseAWSCredentialOverride(ctx, r.Client, r.clusterInfo.region, vpce.Spec.AWSCredentialOverrideRef)
if err != nil {
return err
}
r.awsClient = aws_client.NewAwsClient(cfg)
} else {
// Load the default AWS credentials that are available to the controller
if refreshAWSSession {
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(r.clusterInfo.region))
if err != nil {
return err
}
r.awsClient = aws_client.NewAwsClient(cfg)
}
}

if vpce.Spec.Vpc.AutoDiscoverSubnets {
Expand Down Expand Up @@ -117,6 +128,10 @@ func (r *VpcEndpointReconciler) parseClusterInfo(ctx context.Context, vpce *avov
return errors.New("cannot set both .spec.customDns.route53PrivateHostedZone.id and .spec.customDns.route53PrivateHostedZone.domainName")
}

if vpce.Spec.CustomDns.Route53PrivateHostedZone.Record.Hostname == "" && vpce.Spec.CustomDns.Route53PrivateHostedZone.Record.ExternalNameService.Name != "" {
return errors.New("cannot create an ExternalName service without a Route53 Hosted Zone record")
}

return nil
}

Expand Down
12 changes: 12 additions & 0 deletions deploy/17_role.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: aws-vpce-operator-override-secret
namespace: openshift-aws-vpce-operator
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
13 changes: 13 additions & 0 deletions deploy/18_rolebinding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: aws-vpce-operator-override-secret
namespace: openshift-aws-vpce-operator
subjects:
- kind: ServiceAccount
name: aws-vpce-operator
namespace: openshift-aws-vpce-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: aws-vpce-operator
19 changes: 17 additions & 2 deletions deploy/crds/avo.openshift.io_vpcendpoints.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,21 @@ spec:
description: 'AssumeRoleArn will allow AVO to use sts:AssumeRole to
create VPC Endpoints in separate AWS Accounts TODO: Implement'
type: string
awsCredentialOverrideRef:
description: AWSCredentialOverride is a Kubernetes secret containing
AWS credentials for the operator to use for reconciling this specific
vpcendpoint Custom Resource
properties:
name:
description: name is unique within a namespace to reference a
secret resource.
type: string
namespace:
description: namespace defines the space within which the secret
name must be unique.
type: string
type: object
x-kubernetes-map-type: atomic
customDns:
description: CustomDns will define configurations for all other custom
DNS setups, such as a separate Route 53 Private Hosted Zone or an
Expand All @@ -270,8 +285,8 @@ spec:
Route 53 Private Hosted Zone
type: boolean
domainName:
description: 'DomainName specifies the domain name of a Route
53 Private Hosted Zone to create TODO: Implement'
description: DomainName specifies the domain name of a Route
53 Private Hosted Zone to create
type: string
id:
description: Id specifies the AWS ID of an existing Route
Expand Down
70 changes: 70 additions & 0 deletions pkg/secrets/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
Copyright 2022.
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 secrets

import (
"context"
"encoding/base64"
"errors"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"

corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
defaultAWSAccessKeyId = "aws_access_key_id" //#nosec G101
defaultAWSSecretAccessKey = "aws_secret_access_key" //#nosec G101
)

// ParseAWSCredentialOverride takes in an AWS region and a secret reference and attempts to assemble an aws.Config
// Currently only supports parsing AWS IAM User credentials
func ParseAWSCredentialOverride(ctx context.Context, c client.Client, region string, ref *corev1.SecretReference) (aws.Config, error) {
if ref == nil {
return aws.Config{}, errors.New("AWS Credential Override secret reference must not be nil")
}

secret := new(corev1.Secret)
if err := c.Get(ctx, client.ObjectKey{Namespace: ref.Namespace, Name: ref.Name}, secret); err != nil {
return aws.Config{}, err
}

if b64AccessKeyId, ok := secret.Data[defaultAWSAccessKeyId]; ok {
if b64SecretAccessKey, ok := secret.Data[defaultAWSSecretAccessKey]; ok {
accessKeyId, err := base64.StdEncoding.DecodeString(string(b64AccessKeyId))
if err != nil {
return aws.Config{}, err
}

secretAccessKey, err := base64.StdEncoding.DecodeString(string(b64SecretAccessKey))
if err != nil {
return aws.Config{}, err
}

return config.LoadDefaultConfig(ctx,
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(string(accessKeyId), string(secretAccessKey), "")),
config.WithRegion(region),
)
}
}

return aws.Config{}, fmt.Errorf("could not parse credential override secret, requires data keys %s and %s", defaultAWSAccessKeyId, defaultAWSSecretAccessKey)
}
87 changes: 87 additions & 0 deletions pkg/secrets/secrets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
Copyright 2022.
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 secrets

import (
"context"
"encoding/base64"
"testing"

"github.com/openshift/aws-vpce-operator/pkg/testutil"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
mockAWSAccessKeyId = "mock_access_key_id" //#nosec G101
mockAWSSecretAccessKey = "mock_secret_access_key" //#nosec G101
)

func TestParseAWSCredentialOverride(t *testing.T) {

tests := []struct {
secret *corev1.Secret
expectErr bool
}{
{
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "override",
Namespace: "override-ns",
},
Data: map[string][]byte{
defaultAWSAccessKeyId: []byte(base64.StdEncoding.EncodeToString([]byte(mockAWSAccessKeyId))),
defaultAWSSecretAccessKey: []byte(base64.StdEncoding.EncodeToString([]byte(mockAWSSecretAccessKey))),
},
},
expectErr: false,
},
}

for _, test := range tests {
t.Run("", func(t *testing.T) {
mock := testutil.NewTestMock(t, test.secret)
ref := &corev1.SecretReference{
Name: test.secret.Name,
Namespace: test.secret.Namespace,
}
cfg, err := ParseAWSCredentialOverride(context.TODO(), mock.Client, "us-east-1", ref)
if test.expectErr {
if err == nil {
t.Errorf("expected err, got nil")
}
} else {
if err != nil {
t.Errorf("expected no err, got %v", err)
}

creds, err := cfg.Credentials.Retrieve(context.TODO())
if err != nil {
t.Errorf("unexpected err: %v", err)
}

if creds.AccessKeyID != mockAWSAccessKeyId {
t.Errorf("expected %s, got %s", mockAWSAccessKeyId, creds.AccessKeyID)
}

if creds.SecretAccessKey != mockAWSSecretAccessKey {
t.Errorf("expected %s, got %s", mockAWSSecretAccessKey, creds.SecretAccessKey)
}
}
})
}
}

0 comments on commit 62262f7

Please sign in to comment.