From 44493dacabb40affffb0cb0e54e2780cac0fd2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Blaise?= Date: Mon, 26 Aug 2024 18:46:12 +0200 Subject: [PATCH 1/9] add aws cli and patch from composite and extra resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Clément Blaise --- Dockerfile | 12 ++--- air.toml | 5 ++ env.go | 26 ++++++++++ example/aws/composition.yaml | 48 +++++++++++++++++++ example/aws/extra-resources.yaml | 11 +++++ example/aws/functions.yaml | 18 +++++++ example/aws/xr.yaml | 6 +++ fn.go | 21 +++++++- input/v1alpha1/parameters.go | 6 ++- .../template.fn.crossplane.io_parameters.yaml | 4 ++ 10 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 air.toml create mode 100644 example/aws/composition.yaml create mode 100644 example/aws/extra-resources.yaml create mode 100644 example/aws/functions.yaml create mode 100644 example/aws/xr.yaml diff --git a/Dockerfile b/Dockerfile index fb4162b..2b5aac7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,13 +10,12 @@ ARG GO_VERSION=1 # architecture that we're building the function for. FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} AS build -RUN apt-get update && apt-get install -y coreutils jq unzip zsh -RUN mkdir /scripts && chown 2000:2000 /scripts +RUN apt-get update && apt-get install -y coreutils jq unzip zsh less +RUN mkdir /scripts /.aws && chown 2000:2000 /scripts /.aws -# TODO: Install awscli, gcloud -# RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip" && \ -# unzip "/tmp/awscliv2.zip" && \ -# ./aws/install +RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip" && \ + unzip "/tmp/awscliv2.zip" && \ + ./aws/install WORKDIR /fn @@ -52,6 +51,7 @@ FROM gcr.io/distroless/python3-debian12 AS image WORKDIR / COPY --from=build --chown=2000:2000 /scripts /scripts +COPY --from=build --chown=2000:2000 /.aws /.aws COPY --from=build /bin /bin COPY --from=build /etc /etc diff --git a/air.toml b/air.toml new file mode 100644 index 0000000..de6dc0c --- /dev/null +++ b/air.toml @@ -0,0 +1,5 @@ +# air.toml +root = "." +tmp_dir = "tmp" +build_cmd = "go build -o ./tmp/main ." +run_cmd = "./tmp/main" diff --git a/env.go b/env.go index 0590d8b..e22feb6 100644 --- a/env.go +++ b/env.go @@ -2,9 +2,19 @@ package main import ( "encoding/json" + "fmt" "os" "github.com/crossplane-contrib/function-shell/input/v1alpha1" + "github.com/crossplane/crossplane-runtime/pkg/fieldpath" + fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" + "github.com/crossplane/function-sdk-go/request" + "github.com/crossplane/function-sdk-go/resource" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +const ( + ExtraResourceFunctionContextKeyEnvironment = "apiextensions.crossplane.io/extra-resources" ) func addShellEnvVarsFromRef(envVarsRef v1alpha1.ShellEnvVarsRef, shellEnvVars map[string]string) (map[string]string, error) { @@ -19,3 +29,19 @@ func addShellEnvVarsFromRef(envVarsRef v1alpha1.ShellEnvVarsRef, shellEnvVars ma } return shellEnvVars, nil } + +func fromExtraResourceField(req *fnv1beta1.RunFunctionRequest, path string) (value string, err error) { + var extraResources *unstructured.Unstructured + if v, ok := request.GetContextKey(req, ExtraResourceFunctionContextKeyEnvironment); ok { + extraResources = &unstructured.Unstructured{} + if err = resource.AsObject(v.GetStructValue(), extraResources); err != nil { + return + } + valueRaw, errValue := fieldpath.Pave(extraResources.Object).GetValue(path) + if errValue != nil { + return + } + return fmt.Sprintf("%v", valueRaw), nil + } + return +} diff --git a/example/aws/composition.yaml b/example/aws/composition.yaml new file mode 100644 index 0000000..7c904f0 --- /dev/null +++ b/example/aws/composition.yaml @@ -0,0 +1,48 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: shell-example +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: XR + mode: Pipeline + pipeline: + - step: pull-extra-resources + functionRef: + name: function-extra-resources + input: + apiVersion: extra-resources.fn.crossplane.io/v1beta1 + kind: Input + spec: + extraResources: + - kind: ProviderConfig + into: ProviderConfig + apiVersion: aws.upbound.io/v1beta1 + type: Selector + selector: + maxMatch: 1 + minMatch: 1 + matchLabels: + - key: crossplane.io/claim-namespace + type: Value + value: demo + - step: shell + functionRef: + name: function-shell + input: + apiVersion: shell.fn.crossplane.io/v1alpha1 + kind: Parameters + shellEnvVars: + - key: AWS_ROLE_ARN + value: arn:aws:iam::00000002:role/ProviderAWS + - key: AWS_ASSUME_ROLE_ARN + valueFromExtraResourcesField: "ProviderConfig[0].spec.assumeRoleChain[0].roleARN" + shellCommand: | + ASSUME_ROLE_OUTPUT=$(aws sts assume-role --role-arn $AWS_ASSUME_ROLE_ARN --role-session-name "function-shell") + export AWS_ACCESS_KEY_ID=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"AccessKeyId": "[^"]*"' | cut -d'"' -f4) + export AWS_SECRET_ACCESS_KEY=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"SecretAccessKey": "[^"]*"' | cut -d'"' -f4) + export AWS_SESSION_TOKEN=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"SessionToken": "[^"]*"' | cut -d'"' -f4) + aws eks list-clusters | jq -r '.clusters[0]' + stdoutField: status.atFunction.shell.stdout + stderrField: status.atFunction.shell.stderr diff --git a/example/aws/extra-resources.yaml b/example/aws/extra-resources.yaml new file mode 100644 index 0000000..458d5b6 --- /dev/null +++ b/example/aws/extra-resources.yaml @@ -0,0 +1,11 @@ +apiVersion: aws.upbound.io/v1beta1 +kind: ProviderConfig +metadata: + labels: + account: demo + name: demo +spec: + assumeRoleChain: + - roleARN: arn:aws:iam::00000002:role/Crossplane + credentials: + source: IRSA diff --git a/example/aws/functions.yaml b/example/aws/functions.yaml new file mode 100644 index 0000000..86b46af --- /dev/null +++ b/example/aws/functions.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-shell + annotations: + # This tells crossplane beta render to connect to the function locally. + render.crossplane.io/runtime: Development +spec: + # This is ignored when using the Development runtime. + package: function-shell +--- +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-extra-resources +spec: + package: xpkg.upbound.io/crossplane-contrib/function-extra-resources:v0.0.3 diff --git a/example/aws/xr.yaml b/example/aws/xr.yaml new file mode 100644 index 0000000..8d921d7 --- /dev/null +++ b/example/aws/xr.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: example.crossplane.io/v1 +kind: XR +metadata: + name: example-aws +spec: {} diff --git a/fn.go b/fn.go index 9369d18..aee84b3 100644 --- a/fn.go +++ b/fn.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "fmt" "strings" "github.com/crossplane-contrib/function-shell/input/v1alpha1" @@ -11,7 +12,6 @@ import ( fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" "github.com/crossplane/function-sdk-go/request" "github.com/crossplane/function-sdk-go/response" - "github.com/keegancsmith/shell" ) @@ -87,7 +87,24 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ var shellEnvVars = make(map[string]string) for _, envVar := range in.ShellEnvVars { - shellEnvVars[envVar.Key] = envVar.Value + if envVar.ValueFromCompositeField != "" { + envValue, err := oxr.Resource.GetValue(envVar.ValueFromCompositeField) + if err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot process contents of valueFromCompositeField %s", envVar.ValueFromCompositeField)) + return rsp, nil + } + shellEnvVars[envVar.Key] = fmt.Sprintf("%v", envValue) + } + if envVar.ValueFromExtraResourcesField != "" { + envValue, err := fromExtraResourceField(req, envVar.ValueFromExtraResourcesField) + if err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot process contents of valueFromExtraResourcesField %s", envVar.ValueFromExtraResourcesField)) + return rsp, nil + } + shellEnvVars[envVar.Key] = envValue + } else { + shellEnvVars[envVar.Key] = envVar.Value + } } if len(in.ShellEnvVarsRef.Keys) > 0 { diff --git a/input/v1alpha1/parameters.go b/input/v1alpha1/parameters.go index 3c6aaea..c91a12f 100644 --- a/input/v1alpha1/parameters.go +++ b/input/v1alpha1/parameters.go @@ -45,8 +45,10 @@ type Parameters struct { } type ShellEnvVar struct { - Key string `json:"key,omitempty"` - Value string `json:"value,omitempty"` + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` + ValueFromCompositeField string `json:"valueFromCompositeField,omitempty"` + ValueFromExtraResourcesField string `json:"valueFromExtraResourcesField,omitempty"` } type ShellEnvVarsRef struct { diff --git a/package/input/template.fn.crossplane.io_parameters.yaml b/package/input/template.fn.crossplane.io_parameters.yaml index 1d0fe1b..dcdc951 100644 --- a/package/input/template.fn.crossplane.io_parameters.yaml +++ b/package/input/template.fn.crossplane.io_parameters.yaml @@ -52,6 +52,10 @@ spec: type: string value: type: string + valueFromCompositeFieldPath: + type: string + valueFromExtraResourcesFieldPath: + type: string type: object type: array shellEnvVarsRef: From 26785c6759fc57f22f53ae8c3889332ea8f4340e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Blaise?= Date: Wed, 4 Sep 2024 18:15:38 +0200 Subject: [PATCH 2/9] update to valueRef to allow using generic context name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Clément Blaise --- env.go | 43 ++++++++++++------- example/aws/composition.yaml | 6 +-- fn.go | 15 ++----- input/v1alpha1/parameters.go | 7 ++- .../template.fn.crossplane.io_parameters.yaml | 4 +- 5 files changed, 38 insertions(+), 37 deletions(-) diff --git a/env.go b/env.go index e22feb6..1873988 100644 --- a/env.go +++ b/env.go @@ -4,8 +4,10 @@ import ( "encoding/json" "fmt" "os" + "regexp" "github.com/crossplane-contrib/function-shell/input/v1alpha1" + "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/fieldpath" fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" "github.com/crossplane/function-sdk-go/request" @@ -13,10 +15,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -const ( - ExtraResourceFunctionContextKeyEnvironment = "apiextensions.crossplane.io/extra-resources" -) - func addShellEnvVarsFromRef(envVarsRef v1alpha1.ShellEnvVarsRef, shellEnvVars map[string]string) (map[string]string, error) { var envVarsData map[string]string @@ -30,18 +28,33 @@ func addShellEnvVarsFromRef(envVarsRef v1alpha1.ShellEnvVarsRef, shellEnvVars ma return shellEnvVars, nil } -func fromExtraResourceField(req *fnv1beta1.RunFunctionRequest, path string) (value string, err error) { - var extraResources *unstructured.Unstructured - if v, ok := request.GetContextKey(req, ExtraResourceFunctionContextKeyEnvironment); ok { - extraResources = &unstructured.Unstructured{} - if err = resource.AsObject(v.GetStructValue(), extraResources); err != nil { - return +func fromValueRef(req *fnv1beta1.RunFunctionRequest, path string) (string, error) { + // Check for context key presence and capture context key and path + contextRegex := regexp.MustCompile(`^context\[(.+?)].(.+)$`) + if match := contextRegex.FindStringSubmatch(path); match != nil { + if v, ok := request.GetContextKey(req, match[1]); ok { + context := &unstructured.Unstructured{} + if err := resource.AsObject(v.GetStructValue(), context); err != nil { + return "", errors.Wrapf(err, "cannot convert context to %s", v) + } + value, err := fieldpath.Pave(context.Object).GetValue(match[2]) + if err != nil { + return "", errors.Wrapf(err, "cannot get context value at %s", match[2]) + } + return fmt.Sprintf("%v", value), nil } - valueRaw, errValue := fieldpath.Pave(extraResources.Object).GetValue(path) - if errValue != nil { - return + + } else { + oxr, err := request.GetObservedCompositeResource(req) + if err != nil { + return "", errors.Wrapf(err, "cannot get observed composite resource from %T", req) } - return fmt.Sprintf("%v", valueRaw), nil + value, err := oxr.Resource.GetValue(path) + if err != nil { + return "", errors.Wrapf(err, "cannot get observed composite value at %s", path) + } + return fmt.Sprintf("%v", value), nil + } - return + return "", nil } diff --git a/example/aws/composition.yaml b/example/aws/composition.yaml index 7c904f0..08fd286 100644 --- a/example/aws/composition.yaml +++ b/example/aws/composition.yaml @@ -24,7 +24,7 @@ spec: maxMatch: 1 minMatch: 1 matchLabels: - - key: crossplane.io/claim-namespace + - key: account type: Value value: demo - step: shell @@ -35,9 +35,9 @@ spec: kind: Parameters shellEnvVars: - key: AWS_ROLE_ARN - value: arn:aws:iam::00000002:role/ProviderAWS + value: arn:aws:iam::00000001:role/Crossplane - key: AWS_ASSUME_ROLE_ARN - valueFromExtraResourcesField: "ProviderConfig[0].spec.assumeRoleChain[0].roleARN" + valueRef: "context[apiextensions.crossplane.io/extra-resources].ProviderConfig[0].spec.assumeRoleChain[0].roleARN" shellCommand: | ASSUME_ROLE_OUTPUT=$(aws sts assume-role --role-arn $AWS_ASSUME_ROLE_ARN --role-session-name "function-shell") export AWS_ACCESS_KEY_ID=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"AccessKeyId": "[^"]*"' | cut -d'"' -f4) diff --git a/fn.go b/fn.go index aee84b3..9723bcc 100644 --- a/fn.go +++ b/fn.go @@ -3,7 +3,6 @@ package main import ( "bytes" "context" - "fmt" "strings" "github.com/crossplane-contrib/function-shell/input/v1alpha1" @@ -87,18 +86,10 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ var shellEnvVars = make(map[string]string) for _, envVar := range in.ShellEnvVars { - if envVar.ValueFromCompositeField != "" { - envValue, err := oxr.Resource.GetValue(envVar.ValueFromCompositeField) + if envVar.ValueRef != "" { + envValue, err := fromValueRef(req, envVar.ValueRef) if err != nil { - response.Fatal(rsp, errors.Wrapf(err, "cannot process contents of valueFromCompositeField %s", envVar.ValueFromCompositeField)) - return rsp, nil - } - shellEnvVars[envVar.Key] = fmt.Sprintf("%v", envValue) - } - if envVar.ValueFromExtraResourcesField != "" { - envValue, err := fromExtraResourceField(req, envVar.ValueFromExtraResourcesField) - if err != nil { - response.Fatal(rsp, errors.Wrapf(err, "cannot process contents of valueFromExtraResourcesField %s", envVar.ValueFromExtraResourcesField)) + response.Fatal(rsp, errors.Wrapf(err, "cannot process contents of valueRef %s", envVar.ValueRef)) return rsp, nil } shellEnvVars[envVar.Key] = envValue diff --git a/input/v1alpha1/parameters.go b/input/v1alpha1/parameters.go index c91a12f..17484fc 100644 --- a/input/v1alpha1/parameters.go +++ b/input/v1alpha1/parameters.go @@ -45,10 +45,9 @@ type Parameters struct { } type ShellEnvVar struct { - Key string `json:"key,omitempty"` - Value string `json:"value,omitempty"` - ValueFromCompositeField string `json:"valueFromCompositeField,omitempty"` - ValueFromExtraResourcesField string `json:"valueFromExtraResourcesField,omitempty"` + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` + ValueRef string `json:"valueRef,omitempty"` } type ShellEnvVarsRef struct { diff --git a/package/input/template.fn.crossplane.io_parameters.yaml b/package/input/template.fn.crossplane.io_parameters.yaml index dcdc951..cea0c50 100644 --- a/package/input/template.fn.crossplane.io_parameters.yaml +++ b/package/input/template.fn.crossplane.io_parameters.yaml @@ -52,9 +52,7 @@ spec: type: string value: type: string - valueFromCompositeFieldPath: - type: string - valueFromExtraResourcesFieldPath: + valueRef: type: string type: object type: array From 158f342c28165c89c0bda813240b1f59254d0670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Blaise?= Date: Mon, 16 Sep 2024 17:14:18 +0800 Subject: [PATCH 3/9] add env_test.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Clément Blaise --- env_test.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 env_test.go diff --git a/env_test.go b/env_test.go new file mode 100644 index 0000000..acb7741 --- /dev/null +++ b/env_test.go @@ -0,0 +1,86 @@ +package main + +import ( + "testing" + + fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" + "github.com/crossplane/function-sdk-go/resource" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestFromValueRef(t *testing.T) { + + type args struct { + req *fnv1beta1.RunFunctionRequest + path string + } + + type want struct { + result string + err error + } + + cases := map[string]struct { + reason string + args args + want want + }{ + "FromCompositeValid": { + reason: "If composite path is valid, it should be returned.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{ + "apiVersion": "", + "kind": "", + "spec": { + "foo": "bar" + } + }`), + }, + }, + }, + path: "spec.foo", + }, + want: want{ + result: "bar", + err: nil, + }, + }, + "FromContextValid": { + reason: "If composite path is valid, it should be returned.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Context: resource.MustStructJSON(`{ + "apiextensions.crossplane.io/foo": { + "bar": "baz" + } + }`), + }, + path: "context[apiextensions.crossplane.io/foo].bar", + }, + want: want{ + result: "baz", + err: nil, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + result, err := fromValueRef(tc.args.req, tc.args.path) + + if diff := cmp.Diff(tc.want.result, result, protocmp.Transform()); diff != "" { + t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff) + } + }) + } + +} From dc8ab1143e51c91b045a00f4bfd2cd6323efc17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Blaise?= Date: Tue, 17 Sep 2024 17:32:12 +0800 Subject: [PATCH 4/9] add README.md and update example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Clément Blaise --- ...tra-resources.yaml => ProviderConfig.yaml} | 2 +- example/aws/README.md | 169 ++++++++++++++++++ example/aws/composition.yaml | 4 +- 3 files changed, 172 insertions(+), 3 deletions(-) rename example/aws/{extra-resources.yaml => ProviderConfig.yaml} (70%) create mode 100644 example/aws/README.md diff --git a/example/aws/extra-resources.yaml b/example/aws/ProviderConfig.yaml similarity index 70% rename from example/aws/extra-resources.yaml rename to example/aws/ProviderConfig.yaml index 458d5b6..aaa8ef2 100644 --- a/example/aws/extra-resources.yaml +++ b/example/aws/ProviderConfig.yaml @@ -6,6 +6,6 @@ metadata: name: demo spec: assumeRoleChain: - - roleARN: arn:aws:iam::00000002:role/Crossplane + - roleARN: arn:aws:iam::000000000001:role/eks-test-assume-role credentials: source: IRSA diff --git a/example/aws/README.md b/example/aws/README.md new file mode 100644 index 0000000..f948193 --- /dev/null +++ b/example/aws/README.md @@ -0,0 +1,169 @@ +# Example - AWS + +This example demonstrates how to reuse an existing ProviderConfig to authenticate with AWS using IAM Role for +ServiceAccount (IRSA) and execute a command on a different account using AssumeRole. This architecture is known as Hub +and Spoke. + +## Prerequisites + +### Authenticate using IAM Roles for Service Accounts + +The Amazon Elastic Kubernetes Service (EKS) running the function will need a ServiceAccount authorized to authenticate +with AWS. The steps are details in the configuration of +[provider-upjet-aws](https://github.com/crossplane-contrib/provider-upjet-aws/blob/main/docs/family/Configuration.md#authenticate-using-iam-roles-for-service-accounts). + +### Authorize Hub account on the Spoke Account + +The previous step has created the AWS IAM Role`arn:aws:iam::000000000000:role/eks-test-role`. It now needs to be allowed +to make requests on the target account. + +#### Create an IAM policy + +Define the actions allowed on the target account. For this example, it will be restricted to listing IAM Roles. Adapt +the policy based on your needs. + +Apply the policy using the AWS command-line command: +```bash +aws iam create-policy \ + --policy-name IAMRoleLister \ + --policy-document \ +'{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "iam:ListRoles" + ], + "Resource": "*", + "Effect": "Allow" + } + ] +}' +``` + +Example output: +```json +{ + "Policy": { + "PolicyName": "IAMRoleLister", + "PolicyId": "ANPAS43KCNAZNTZIWNOCU", + "Arn": "arn:aws:iam::000000000001:policy/IAMRoleLister", + "Path": "/", + "DefaultVersionId": "v1", + "AttachmentCount": 0, + "PermissionsBoundaryUsageCount": 0, + "IsAttachable": true, + "CreateDate": "2024-09-17T08:35:55+00:00", + "UpdateDate": "2024-09-17T08:35:55+00:00" + } +} +``` + +#### Create an IAM role + +Define the trust policy to allow the Hub account (000000000000) to AssumeRole on the target account (000000000001). + +```bash +aws iam create-role --profile w3f-web3factory-demo \ + --role-name eks-test-assume-role \ + --assume-role-policy-document \ +'{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::000000000000:role/ProviderAWS" + }, + "Action": "sts:AssumeRole", + "Condition": {} + } + ] +}' +``` + +Example output: +```json +{ + "Role": { + "Path": "/", + "RoleName": "eks-test-assume-role", + "RoleId": "AROAS43KCNAZGWLU4RRHM", + "Arn": "arn:aws:iam::000000000001:role/eks-test-assume-role", + "CreateDate": "2024-09-17T08:48:30+00:00", + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::000000000000:role/ProviderAWS" + }, + "Action": "sts:AssumeRole", + "Condition": {} + } + ] + } + } +} +``` + +Attach the IAMRoleLister policy on the role + +```bash +aws iam attach-role-policy \ + --policy-arn arn:aws:iam::000000000001:policy/IAMRoleLister \ + --role-name eks-test-assume-role + ``` + +## Usage + +### Loading ProviderConfig with function-extra-resources + +The composition pipeline use `function-extra-resources` to load ProviderConfig defined in the EKS cluster. For this example +the following has been applied: + +```yaml +apiVersion: aws.upbound.io/v1beta1 +kind: ProviderConfig +metadata: + labels: + account: demo + name: demo +spec: + assumeRoleChain: + - roleARN: arn:aws:iam::000000000001:role/eks-test-assume-role + credentials: + source: IRSA +``` + +The IAM role of the target account is placed at `spec.assumeRoleChain[0].roleARN` + +The `function-extra-resources` use a selector to discover `ProviderConfig`, `maxMatch: 1` and `minMatch: 1`have been set +so that exactly one `ProviderConfig` is return. In case none is found the composition will error out. + +### Configuring function-shell + +AWS CLI is configured with the following environment variable +- **AWS_ROLE_ARN** : is the role created on the hub account +- **AWS_ASSUME_ROLE_ARN**: is the role on the targeted account + +The **AWS_ASSUME_ROLE_ARN** is configured dynamically with `valueRef` with the following expression: +`context[apiextensions.crossplane.io/extra-resources].ProviderConfig[0].spec.assumeRoleChain[0].roleARN` + +The reference is retrieving from the context at the `apiextensions.crossplane.io/extra-resources`key. In the previous step +the `ProviderConfig` are saved into the `ProviderConfig` key (set with `function-extra-resources` input spec.extraResources[0].into) + +For the function to be authenticated it first request a temporary token using: +```bash +aws sts assume-role --role-arn $AWS_ASSUME_ROLE_ARN --role-session-name "function-shell" +``` + +The environment variable `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` are set from the output + +The sessions name can be set to an arbitrary value; it enables keeping track of the service making the call. + +Finally, the AWS command is executed and filter with `jq` to retrieve the ARN of all the roles in the target account with: +```bash +aws iam list-roles | jq -r '.Roles[] .Arn +``` diff --git a/example/aws/composition.yaml b/example/aws/composition.yaml index 08fd286..e32142b 100644 --- a/example/aws/composition.yaml +++ b/example/aws/composition.yaml @@ -35,7 +35,7 @@ spec: kind: Parameters shellEnvVars: - key: AWS_ROLE_ARN - value: arn:aws:iam::00000001:role/Crossplane + value: arn:aws:iam::000000000000:role/eks-test-role - key: AWS_ASSUME_ROLE_ARN valueRef: "context[apiextensions.crossplane.io/extra-resources].ProviderConfig[0].spec.assumeRoleChain[0].roleARN" shellCommand: | @@ -43,6 +43,6 @@ spec: export AWS_ACCESS_KEY_ID=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"AccessKeyId": "[^"]*"' | cut -d'"' -f4) export AWS_SECRET_ACCESS_KEY=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"SecretAccessKey": "[^"]*"' | cut -d'"' -f4) export AWS_SESSION_TOKEN=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"SessionToken": "[^"]*"' | cut -d'"' -f4) - aws eks list-clusters | jq -r '.clusters[0]' + aws iam list-roles | jq -r '.Roles[] .Arn' stdoutField: status.atFunction.shell.stdout stderrField: status.atFunction.shell.stderr From 3f8730e2de9c0d954cff76bdfd0807d996f8c7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Blaise?= Date: Tue, 17 Sep 2024 17:34:32 +0800 Subject: [PATCH 5/9] fix typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Clément Blaise --- example/aws/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example/aws/README.md b/example/aws/README.md index f948193..3d2af18 100644 --- a/example/aws/README.md +++ b/example/aws/README.md @@ -1,6 +1,6 @@ # Example - AWS -This example demonstrates how to reuse an existing ProviderConfig to authenticate with AWS using IAM Role for +This example demonstrates how to reuse an existing `ProviderConfig` to authenticate with AWS using IAM Role for ServiceAccount (IRSA) and execute a command on a different account using AssumeRole. This architecture is known as Hub and Spoke. @@ -64,7 +64,7 @@ Example output: Define the trust policy to allow the Hub account (000000000000) to AssumeRole on the target account (000000000001). ```bash -aws iam create-role --profile w3f-web3factory-demo \ +aws iam create-role \ --role-name eks-test-assume-role \ --assume-role-policy-document \ '{ @@ -73,7 +73,7 @@ aws iam create-role --profile w3f-web3factory-demo \ { "Effect": "Allow", "Principal": { - "AWS": "arn:aws:iam::000000000000:role/ProviderAWS" + "AWS": "arn:aws:iam::000000000000:role/eks-test-role" }, "Action": "sts:AssumeRole", "Condition": {} @@ -97,7 +97,7 @@ Example output: { "Effect": "Allow", "Principal": { - "AWS": "arn:aws:iam::000000000000:role/ProviderAWS" + "AWS": "arn:aws:iam::000000000000:role/eks-test-role" }, "Action": "sts:AssumeRole", "Condition": {} From 7e61d2f5c15f86ea32273eafaa8ab349df2eab8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Blaise?= Date: Tue, 17 Sep 2024 17:36:06 +0800 Subject: [PATCH 6/9] fix typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Clément Blaise --- example/aws/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/aws/README.md b/example/aws/README.md index 3d2af18..9c59347 100644 --- a/example/aws/README.md +++ b/example/aws/README.md @@ -152,7 +152,7 @@ The **AWS_ASSUME_ROLE_ARN** is configured dynamically with `valueRef` with the f `context[apiextensions.crossplane.io/extra-resources].ProviderConfig[0].spec.assumeRoleChain[0].roleARN` The reference is retrieving from the context at the `apiextensions.crossplane.io/extra-resources`key. In the previous step -the `ProviderConfig` are saved into the `ProviderConfig` key (set with `function-extra-resources` input spec.extraResources[0].into) +the `ProviderConfig` are saved into the `ProviderConfig` key (set with `function-extra-resources` input `spec.extraResources[0].into`) For the function to be authenticated it first request a temporary token using: ```bash From 8ed77e5f27b7231a49de030a42a84b65b385a4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Blaise?= Date: Tue, 17 Sep 2024 17:38:11 +0800 Subject: [PATCH 7/9] add missing commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Clément Blaise --- example/aws/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/example/aws/README.md b/example/aws/README.md index 9c59347..e11f676 100644 --- a/example/aws/README.md +++ b/example/aws/README.md @@ -156,12 +156,17 @@ the `ProviderConfig` are saved into the `ProviderConfig` key (set with `function For the function to be authenticated it first request a temporary token using: ```bash -aws sts assume-role --role-arn $AWS_ASSUME_ROLE_ARN --role-session-name "function-shell" +ASSUME_ROLE_OUTPUT=$(aws sts assume-role --role-arn $AWS_ASSUME_ROLE_ARN --role-session-name "function-shell") ``` -The environment variable `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` are set from the output +The session name can be set to an arbitrary value; it enables keeping track of the service making the call. -The sessions name can be set to an arbitrary value; it enables keeping track of the service making the call. +The environment variable `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` are set from the output with: +```bash +export AWS_ACCESS_KEY_ID=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"AccessKeyId": "[^"]*"' | cut -d'"' -f4) +export AWS_SECRET_ACCESS_KEY=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"SecretAccessKey": "[^"]*"' | cut -d'"' -f4) +export AWS_SESSION_TOKEN=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"SessionToken": "[^"]*"' | cut -d'"' -f4) +``` Finally, the AWS command is executed and filter with `jq` to retrieve the ARN of all the roles in the target account with: ```bash From 098122a0d560756dce68082e399267d9316c57e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Blaise?= Date: Wed, 18 Sep 2024 16:06:36 +0800 Subject: [PATCH 8/9] fix typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Clément Blaise --- example/aws/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/aws/README.md b/example/aws/README.md index e11f676..3543790 100644 --- a/example/aws/README.md +++ b/example/aws/README.md @@ -168,7 +168,7 @@ export AWS_SECRET_ACCESS_KEY=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"SecretAccess export AWS_SESSION_TOKEN=$(echo $ASSUME_ROLE_OUTPUT | grep -o '"SessionToken": "[^"]*"' | cut -d'"' -f4) ``` -Finally, the AWS command is executed and filter with `jq` to retrieve the ARN of all the roles in the target account with: +Finally, the AWS command is executed and filtered with `jq` to retrieve the ARN of all the roles in the target account with: ```bash -aws iam list-roles | jq -r '.Roles[] .Arn +aws iam list-roles | jq -r '.Roles[] .Arn' ``` From ea1f2feb9077dd17cb949c6f65e81a52138b70f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Blaise?= Date: Fri, 20 Sep 2024 12:27:39 +0800 Subject: [PATCH 9/9] remove leading space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Clément Blaise --- example/aws/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/aws/README.md b/example/aws/README.md index 3543790..eb0a5d3 100644 --- a/example/aws/README.md +++ b/example/aws/README.md @@ -114,7 +114,7 @@ Attach the IAMRoleLister policy on the role aws iam attach-role-policy \ --policy-arn arn:aws:iam::000000000001:policy/IAMRoleLister \ --role-name eks-test-assume-role - ``` +``` ## Usage