diff --git a/api/v1beta2/ociclusteridentity_types.go b/api/v1beta2/ociclusteridentity_types.go index 13cfd741..4115585c 100644 --- a/api/v1beta2/ociclusteridentity_types.go +++ b/api/v1beta2/ociclusteridentity_types.go @@ -29,6 +29,8 @@ const ( UserPrincipal PrincipalType = "UserPrincipal" // InstancePrincipal represents a instance principal. InstancePrincipal PrincipalType = "InstancePrincipal" + // WorkloadPrincipal represents a workload principal. + WorkloadPrincipal PrincipalType = "Workload" ) // OCIClusterIdentitySpec defines the parameters that are used to create an OCIClusterIdentity. diff --git a/cloud/util/util.go b/cloud/util/util.go index c574cff5..68459a77 100644 --- a/cloud/util/util.go +++ b/cloud/util/util.go @@ -20,7 +20,11 @@ import ( "context" "crypto/x509" "fmt" + "io" + "net/http" + "os" "reflect" + "time" "github.com/go-logr/logr" infrastructurev1beta2 "github.com/oracle/cluster-api-provider-oci/api/v1beta2" @@ -41,6 +45,15 @@ import ( "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + instanceMetadataRegionInfoURLV2 = "http://169.254.169.254/opc/v2/instance/regionInfo/regionIdentifier" +) + +var ( + currentRegion *string ) // GetClusterIdentityFromRef returns the OCIClusterIdentity referenced by the OCICluster. @@ -97,6 +110,7 @@ func getOCIClientCertPool(ctx context.Context, c client.Client, namespace string // GetOrBuildClientFromIdentity creates ClientProvider from OCIClusterIdentity object func GetOrBuildClientFromIdentity(ctx context.Context, c client.Client, identity *infrastructurev1beta2.OCIClusterIdentity, defaultRegion string, clientOverrides *infrastructurev1beta2.ClientOverrides, namespace string) (*scope.ClientProvider, error) { + logger := log.FromContext(ctx) if identity.Spec.Type == infrastructurev1beta2.UserPrincipal { secretRef := identity.Spec.PrincipalSecret key := types.NamespacedName{ @@ -155,6 +169,42 @@ func GetOrBuildClientFromIdentity(ctx context.Context, c client.Client, identity OciAuthConfigProvider: provider, ClientOverrides: clientOverrides}) + if err != nil { + return nil, err + } + return clientProvider, nil + } else if identity.Spec.Type == infrastructurev1beta2.WorkloadPrincipal { + _, containsVersion := os.LookupEnv(auth.ResourcePrincipalVersionEnvVar) + if !containsVersion { + os.Setenv(auth.ResourcePrincipalVersionEnvVar, auth.ResourcePrincipalVersion2_2) + } + _, containsRegion := os.LookupEnv(auth.ResourcePrincipalRegionEnvVar) + if !containsRegion { + // initialize the current region from region metadata + if currentRegion == nil { + regionByte, err := getRegionInfoFromInstanceMetadataServiceProd() + if err != nil { + return nil, err + } + currentRegion = common.String(string(regionByte)) + } + logger.Info(fmt.Sprintf("Looked up region %s from instance metadata", *currentRegion)) + os.Setenv(auth.ResourcePrincipalRegionEnvVar, *currentRegion) + } + + provider, err := auth.OkeWorkloadIdentityConfigurationProvider() + if err != nil { + return nil, err + } + pool, err := getOCIClientCertPool(ctx, c, namespace, clientOverrides) + if err != nil { + return nil, err + } + clientProvider, err := scope.NewClientProvider(scope.ClientProviderParams{ + CertOverride: pool, + OciAuthConfigProvider: provider, + ClientOverrides: clientOverrides}) + if err != nil { return nil, err } @@ -406,6 +456,35 @@ func DeleteOrphanedMachinePoolMachines(ctx context.Context, params MachineParams return nil } +func getRegionInfoFromInstanceMetadataServiceProd() ([]byte, error) { + request, err := http.NewRequest(http.MethodGet, instanceMetadataRegionInfoURLV2, nil) + request.Header.Add("Authorization", "Bearer Oracle") + + client := &http.Client{ + Timeout: time.Second * 10, + } + resp, err := client.Do(request) + if err != nil { + return nil, errors.Wrap(err, "failed to call instance metadata service") + } + + statusCode := resp.StatusCode + + defer resp.Body.Close() + + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to get region information from response body") + } + + if statusCode != http.StatusOK { + err = fmt.Errorf("HTTP Get failed: URL: %s, Status: %s, Message: %s", + instanceMetadataRegionInfoURLV2, resp.Status, string(content)) + return nil, err + } + + return content, nil +} // MachineParams specifies the params required to create or delete machinepool machines. // Infra machine pool specifed below refers to OCIManagedMachinePool/OCIMachinePool/OCIVirtualMachinePool diff --git a/docs/src/gs/multi-tenancy.md b/docs/src/gs/multi-tenancy.md index 728b4ad8..97c7047c 100644 --- a/docs/src/gs/multi-tenancy.md +++ b/docs/src/gs/multi-tenancy.md @@ -83,5 +83,22 @@ spec: allowedNamespaces: {} ``` +## Cluster Identity using Workload Identity + +Cluster Identity supports [Workload][workload] access to OCI resources also knows as Workload Identity. The example +`OCIClusterIdentity` spec shown below uses Workload Identity. + +```yaml +--- +kind: OCIClusterIdentity +metadata: + name: cluster-identity + namespace: default +spec: + type: Workload + allowedNamespaces: {} +``` + [iam-user]: https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm#Required_Keys_and_OCIDs -[instance-principals]: https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm \ No newline at end of file +[instance-principals]: https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm +[workload]: https://docs.oracle.com/en-us/iaas/Content/ContEng/Tasks/contenggrantingworkloadaccesstoresources.htm \ No newline at end of file