diff --git a/.devcontainer/update-devcontainer-image.sh b/.devcontainer/update-devcontainer-image.sh index c35db12e..aa85f1d5 100755 --- a/.devcontainer/update-devcontainer-image.sh +++ b/.devcontainer/update-devcontainer-image.sh @@ -5,7 +5,6 @@ set -euo pipefail -azure_subscription="BiomedicalImaging-NonProd" devcontainer_registry_subdomain="compimagdevcontainers" devcontainer_registry="${devcontainer_registry_subdomain}.azurecr.io" devcontainer_repository="${devcontainer_registry}/tyger" diff --git a/cli/go.mod b/cli/go.mod index f4535c52..41bbcd36 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -30,7 +30,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.19.0 - helm.sh/helm/v3 v3.13.0 + helm.sh/helm/v3 v3.12.3 k8s.io/api v0.28.2 k8s.io/apimachinery v0.28.2 k8s.io/client-go v0.28.2 diff --git a/cli/go.sum b/cli/go.sum index c46d83ea..59b65068 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -709,8 +709,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -helm.sh/helm/v3 v3.13.0 h1:XPJKIU30K4JTQ6VX/6e0hFAmEIonYa8E7wx5aqv4xOc= -helm.sh/helm/v3 v3.13.0/go.mod h1:2PBEKsMWKLVZTojUOqMS3Eadv5mP43FBWrRgLNkNm9Y= +helm.sh/helm/v3 v3.12.3 h1:5y1+Sbty12t48T/t/CGNYUIME5BJ0WKfmW/sobYqkFg= +helm.sh/helm/v3 v3.12.3/go.mod h1:KPKQiX9IP5HX7o5YnnhViMnNuKiL/lJBVQ47GHe1R0k= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw= k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg= diff --git a/cli/internal/cmd/config.go b/cli/internal/cmd/config.go index b5ad2141..1ac92bc4 100644 --- a/cli/internal/cmd/config.go +++ b/cli/internal/cmd/config.go @@ -104,7 +104,7 @@ func newConfigCreateCommand() *cobra.Command { for { for { - templateValues.CurrentUserId, templateValues.CurrentUserDisplayName, err = getCurrentPrincipal(cred) + templateValues.PrincipalId, templateValues.PrincipalDisplayName, templateValues.PrincipalKind, err = getCurrentPrincipal(cred) if err != nil { if err == errNotLoggedIn { fmt.Printf("You are not logged in to Azure. Please run `az login` in another terminal window.\nPress any key to continue when ready...\n\n") @@ -116,7 +116,7 @@ func newConfigCreateCommand() *cobra.Command { break } - input := confirmation.New(fmt.Sprintf("You are logged in as %s. Is that the right account?", templateValues.CurrentUserDisplayName), confirmation.Yes) + input := confirmation.New(fmt.Sprintf("You are logged in as %s. Is that the right account?", templateValues.PrincipalDisplayName), confirmation.Yes) ready, err := input.RunPrompt() if err != nil { return err @@ -306,42 +306,42 @@ func chooseSubscription(cred azcore.TokenCredential) (string, error) { return sub.id, nil } -func getCurrentPrincipal(cred azcore.TokenCredential) (oid, display string, err error) { +func getCurrentPrincipal(cred azcore.TokenCredential) (oid, display string, kind install.PrincipalKind, err error) { tokenResponse, err := cred.GetToken(context.Background(), policy.TokenRequestOptions{Scopes: []string{cloud.AzurePublic.Services[cloud.ResourceManager].Audience}}) if err != nil { - return "", "", errNotLoggedIn + return "", "", "", errNotLoggedIn } claims := jwt.MapClaims{} _, _, err = jwt.NewParser().ParseUnverified(tokenResponse.Token, claims) if err != nil { - return "", "", fmt.Errorf("failed to parse token: %w", err) + return "", "", "", fmt.Errorf("failed to parse token: %w", err) } oid = claims["oid"].(string) if unique_name, ok := claims["unique_name"]; ok { display = unique_name.(string) - return oid, display, nil + return oid, display, install.PrincipalKindUser, nil } principals, err := install.ObjectsIdToPrincipals(context.Background(), []string{oid}) if err != nil { - return "", "", err + return "", "", "", err } if len(principals) == 0 { - return "", "", errors.New("principal not found") + return "", "", "", errors.New("principal not found") } if principals[0].Kind != install.PrincipalKindServicePrincipal { - return "", "", errors.New("principal is not a service principal") + return "", "", "", errors.New("principal was expected to be a service principal but isn't") } - display, err = install.GetServicePrincipalDisplayName(context.Background(), principals[0].Id) + display, err = install.GetServicePrincipalDisplayName(context.Background(), principals[0].ObjectId) if err != nil { - return "", "", err + return "", "", "", err } - return oid, display, nil + return oid, display, install.PrincipalKindServicePrincipal, nil } func chooseTenant(cred azcore.TokenCredential, prompt string, presentOtherOption bool) (string, error) { diff --git a/cli/internal/install/config.go b/cli/internal/install/config.go index cb827cca..effa981b 100644 --- a/cli/internal/install/config.go +++ b/cli/internal/install/config.go @@ -26,10 +26,23 @@ type CloudConfig struct { type ComputeConfig struct { Clusters []*ClusterConfig `json:"clusters"` - ManagementPrincipalIds []string `json:"managementPrincipalIds"` + ManagementPrincipals []Principal `json:"managementPrincipals"` PrivateContainerRegistries []string `json:"privateContainerRegistries"` } +type PrincipalKind string + +const ( + PrincipalKindUser PrincipalKind = "User" + PrincipalKindGroup PrincipalKind = "Group" + PrincipalKindServicePrincipal PrincipalKind = "ServicePrincipal" +) + +type Principal struct { + ObjectId string `json:"objectId"` + Kind PrincipalKind `json:"kind"` +} + func (c *ComputeConfig) GetApiHostCluster() *ClusterConfig { for _, c := range c.Clusters { if c.ApiHost { @@ -99,8 +112,9 @@ type ConfigTemplateValues struct { TenantId string SubscriptionId string DefaultLocation string - CurrentUserId string - CurrentUserDisplayName string + PrincipalId string + PrincipalDisplayName string + PrincipalKind PrincipalKind BufferStorageAccountName string LogsStorageAccountName string DomainName string diff --git a/cli/internal/install/config.tpl b/cli/internal/install/config.tpl index bc3171db..b4450bd9 100644 --- a/cli/internal/install/config.tpl +++ b/cli/internal/install/config.tpl @@ -25,9 +25,10 @@ cloud: # These are the principals that will be granted full access to the # "tyger" namespace in each cluster. managementPrincipalIds: - - {{ .CurrentUserId }} # {{ .CurrentUserDisplayName }} + - objectId: {{ .CurrentUserId }} # {{ .CurrentUserDisplayName }} + type: {{ .PrincipalKind }} - # Specify the names of private container registries that the clusters must be able to pull from + # The names of private container registries that the clusters must be able to pull from # privateContainerRegistries: # - myprivateregistry diff --git a/cli/internal/install/graph.go b/cli/internal/install/graph.go index 055d42c8..1c57f8cf 100644 --- a/cli/internal/install/graph.go +++ b/cli/internal/install/graph.go @@ -14,21 +14,8 @@ import ( "github.com/rs/zerolog/log" ) -type PrincipalKind string - -const ( - PrincipalKindUser PrincipalKind = "User" - PrincipalKindGroup PrincipalKind = "Group" - PrincipalKindServicePrincipal PrincipalKind = "ServicePrincipal" -) - var errNotFound = fmt.Errorf("not found") -type Principal struct { - Id string - Kind PrincipalKind -} - func GetGraphToken(ctx context.Context) (azcore.AccessToken, error) { cred := GetAzureCredentialFromContext(ctx) tokenResponse, err := cred.GetToken(ctx, policy.TokenRequestOptions{ @@ -163,8 +150,8 @@ func ObjectsIdToPrincipals(ctx context.Context, objectIds []string) ([]Principal } principal = &Principal{ - Id: value.Id, - Kind: kind, + ObjectId: value.Id, + Kind: kind, } break } diff --git a/cli/internal/install/kubernetes.go b/cli/internal/install/kubernetes.go index c7cc2ceb..9e383bae 100644 --- a/cli/internal/install/kubernetes.go +++ b/cli/internal/install/kubernetes.go @@ -35,13 +35,6 @@ func createTygerNamespace(ctx context.Context, restConfigPromise *Promise[*rest. func createTygerClusterRBAC(ctx context.Context, restConfigPromise *Promise[*rest.Config], createTygerNamespacePromise *Promise[any]) (any, error) { config := GetConfigFromContext(ctx) - - // we can fetch the principals from the Graph API while we wait for the cluster to be ready - principals, err := ObjectsIdToPrincipals(ctx, config.Cloud.Compute.ManagementPrincipalIds) - if err != nil { - return nil, err - } - restConfig, err := restConfigPromise.Await() if err != nil { return nil, errDependencyFailed @@ -85,9 +78,9 @@ func createTygerClusterRBAC(ctx context.Context, restConfigPromise *Promise[*res Subjects: make([]rbacv1.Subject, 0), } - for _, principal := range principals { + for _, principal := range config.Cloud.Compute.ManagementPrincipals { subject := rbacv1.Subject{ - Name: principal.Id, + Name: principal.ObjectId, Namespace: TygerNamespace, } if principal.Kind == PrincipalKindGroup { diff --git a/cli/internal/install/validation.go b/cli/internal/install/validation.go index fef31c8b..09e7bb5d 100644 --- a/cli/internal/install/validation.go +++ b/cli/internal/install/validation.go @@ -121,6 +121,20 @@ func quickValidateComputeConfig(success *bool, cloudConfig *CloudConfig) { if !hasApiHost { validationError(success, "One cluster must have `apiHost` set to true") } + + if len(computeConfig.ManagementPrincipals) == 0 { + validationError(success, "At least one management principal is required") + } + + for _, p := range computeConfig.ManagementPrincipals { + switch p.Kind { + case PrincipalKindUser, PrincipalKindGroup, PrincipalKindServicePrincipal: + case "": + validationError(success, "The `kind` field is required on a management principal") + default: + validationError(success, "The `kind` field must be one of %v", []PrincipalKind{PrincipalKindUser, PrincipalKindGroup, PrincipalKindServicePrincipal}) + } + } } func quickValidateStorageConfig(success *bool, cloudConfig *CloudConfig) { diff --git a/deploy/config/microsoft/ci/scaled.cue b/deploy/config/microsoft/ci/scaled.cue index 21ffbbc9..9e7885be 100644 --- a/deploy/config/microsoft/ci/scaled.cue +++ b/deploy/config/microsoft/ci/scaled.cue @@ -1,5 +1,5 @@ package tyger #NodePoolConfig: { - minCount: *1 | int + minCount: *1 | int } diff --git a/deploy/config/microsoft/microsoft.cue b/deploy/config/microsoft/microsoft.cue index 9a42847f..eccf73d8 100644 --- a/deploy/config/microsoft/microsoft.cue +++ b/deploy/config/microsoft/microsoft.cue @@ -25,9 +25,15 @@ config: #EnvironmentConfig & { ] }, ] - managementPrincipalIds: [ - "5b60f594-a0eb-410c-a3fc-dd3c6f4e28d1", - "c0e60aba-35f0-4778-bc9b-fc5d2af14687", + managementPrincipals: [ + { + objectId: "5b60f594-a0eb-410c-a3fc-dd3c6f4e28d1" + kind: "ServicePrincipal" + }, + { + objectId: "c0e60aba-35f0-4778-bc9b-fc5d2af14687" + kind: "Group" + }, ] privateContainerRegistries: [developerConfig.wipContainerRegistry.name] } diff --git a/deploy/config/schema.cue b/deploy/config/schema.cue index 0e4cc58d..c11fc416 100644 --- a/deploy/config/schema.cue +++ b/deploy/config/schema.cue @@ -31,9 +31,14 @@ import "strings" storage!: #StorageConfig } +#Principal: { + objectId!: string + kind!: "User" | "Group" | "ServicePrincipal" +} + #ComputeConfig: { clusters!: [...#ClusterConfig] - managementPrincipalIds?: [...string] + managementPrincipals?: [...#Principal] privateContainerRegistries?: [...string] }