diff --git a/.tools/nvim/__http__/accounts/accounts.graphql.yml b/.tools/nvim/__http__/accounts/accounts.graphql.yml index b352865c4..731425bb0 100644 --- a/.tools/nvim/__http__/accounts/accounts.graphql.yml +++ b/.tools/nvim/__http__/accounts/accounts.graphql.yml @@ -1,6 +1,6 @@ --- -global: - accountName: kloudlite-dev +# global: +# accountName: kloudlite-dev --- label: List Accounts @@ -12,6 +12,9 @@ query: |+ displayName creationTime markedForDeletion + createdBy { + userEmail + } metadata{ # labels name @@ -22,7 +25,7 @@ query: |+ --- label: Get Account -query: |+ +query: |+ #graphql query Accounts_getAccount($accountName: String!) { accounts_getAccount(accountName: $accountName) { metadata { @@ -30,29 +33,25 @@ query: |+ } } } -variables: - # accountName: "{{.accountName}}" - accountName: "sample" +variables: + accountName: "{{.accountName}}" --- label: Create Account query: | mutation Accounts_createAccount($account: AccountIn!) { accounts_createAccount(account: $account) { - apiVersion id isActive - kind } } variables: account: contactEmail: "{{.email}}" - displayName: "Sample Account" + displayName: "Example Account Name" metadata: name: "{{.accountName}}" - spec: {} --- @@ -99,7 +98,7 @@ variables: --- lable: Check Name Availability -query: |+ +query: |+ #graphql query Accounts_checkNameAvailability($name: String!) { accounts_checkNameAvailability(name: $name) { result @@ -110,6 +109,16 @@ variables: name: sample --- +label: Ensure Kloudlite Registry Pull Secrets +query: |+ #graphql + query Query($accountName: String!) { + accounts_ensureKloudliteRegistryPullSecrets(accountName: $accountName) + } +variables: + accountName: "{{.accountName}}" + +--- + label: Delete Account query: |+ mutation Accounts_deleteAccount($accountName: String!) { diff --git a/.tools/nvim/dap/go.lua b/.tools/nvim/dap/go.lua index 8d398304d..1b11c2dfb 100644 --- a/.tools/nvim/dap/go.lua +++ b/.tools/nvim/dap/go.lua @@ -105,7 +105,8 @@ dap.configurations.go = { request = "launch", program = vim.g.nxt.project_root_dir .. "/apps/accounts", args = { "--dev" }, - console = "externalTerminal", + -- console = "externalTerminal", + console = "internalTerminal", -- externalTerminal = true, envFile = { vim.g.nxt.project_root_dir .. "/apps/accounts" .. "/.secrets/env", @@ -164,7 +165,7 @@ dap.configurations.go = { name = "Debug Tenant Agent", request = "launch", program = vim.g.nxt.project_root_dir .. "/apps/tenant-agent", - args = { "--dev"}, + args = { "--dev" }, console = "externalTerminal", -- externalTerminal = true, envFile = { @@ -177,7 +178,7 @@ dap.configurations.go = { name = "Debug Websocket Server", request = "launch", program = vim.g.nxt.project_root_dir .. "/apps/websocket-server", - args = { "--dev"}, + args = { "--dev" }, console = "externalTerminal", -- externalTerminal = true, envFile = { diff --git a/.tools/taskfiles/go-build.yml b/.tools/taskfiles/go-build.yml index 110e56b15..38b68cac7 100644 --- a/.tools/taskfiles/go-build.yml +++ b/.tools/taskfiles/go-build.yml @@ -8,9 +8,14 @@ tasks: BuiltAt: sh: date | sed 's/\s/_/g' Dir: "." + upx: '{{.upx | default "true" }}' preconditions: - sh: '[ -n "{{.Out}}" ]' msg: var Out must have a value cmds: - go build -v -ldflags="-s -w -X github.com/kloudlite/api/common.BuiltAt=\"{{.BuiltAt}}\"" -o {{.Out}} {{.Dir}} - - upx {{.Out}} + # - upx --best --lzma {{.Out}} + - |+ + if [ "{{.upx}}" == "true" ]; then + upx {{.Out}} + fi diff --git a/apps/accounts/internal/app/app.go b/apps/accounts/internal/app/app.go index 7f975bcf2..b0ac915e9 100644 --- a/apps/accounts/internal/app/app.go +++ b/apps/accounts/internal/app/app.go @@ -12,6 +12,7 @@ import ( "github.com/kloudlite/api/apps/accounts/internal/env" "github.com/kloudlite/api/common" "github.com/kloudlite/api/constants" + "github.com/kloudlite/api/grpc-interfaces/container_registry" "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/accounts" "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/auth" "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/comms" @@ -61,6 +62,10 @@ var Module = fx.Module("app", return auth.NewAuthClient(conn) }), + fx.Provide(func(conn ContainerRegistryClient) container_registry.ContainerRegistryClient { + return container_registry.NewContainerRegistryClient(conn) + }), + fx.Provide(func(d domain.Domain) accounts.AccountsServer { return &accountsGrpcServer{d: d} }), diff --git a/apps/accounts/internal/app/graph/generated/generated.go b/apps/accounts/internal/app/graph/generated/generated.go index 465dcc6da..c7d336342 100644 --- a/apps/accounts/internal/app/graph/generated/generated.go +++ b/apps/accounts/internal/app/graph/generated/generated.go @@ -149,18 +149,19 @@ type ComplexityRoot struct { } Query struct { - AccountsCheckNameAvailability func(childComplexity int, name string) int - AccountsGetAccount func(childComplexity int, accountName string) int - AccountsGetAccountMembership func(childComplexity int, accountName string) int - AccountsGetInvitation func(childComplexity int, accountName string, invitationID string) int - AccountsListAccounts func(childComplexity int) int - AccountsListInvitations func(childComplexity int, accountName string) int - AccountsListInvitationsForUser func(childComplexity int, onlyPending bool) int - AccountsListMembershipsForAccount func(childComplexity int, accountName string, role *types.Role) int - AccountsListMembershipsForUser func(childComplexity int) int - AccountsResyncAccount func(childComplexity int, accountName string) int - __resolve__service func(childComplexity int) int - __resolve_entities func(childComplexity int, representations []map[string]interface{}) int + AccountsCheckNameAvailability func(childComplexity int, name string) int + AccountsEnsureKloudliteRegistryPullSecrets func(childComplexity int, accountName string) int + AccountsGetAccount func(childComplexity int, accountName string) int + AccountsGetAccountMembership func(childComplexity int, accountName string) int + AccountsGetInvitation func(childComplexity int, accountName string, invitationID string) int + AccountsListAccounts func(childComplexity int) int + AccountsListInvitations func(childComplexity int, accountName string) int + AccountsListInvitationsForUser func(childComplexity int, onlyPending bool) int + AccountsListMembershipsForAccount func(childComplexity int, accountName string, role *types.Role) int + AccountsListMembershipsForUser func(childComplexity int) int + AccountsResyncAccount func(childComplexity int, accountName string) int + __resolve__service func(childComplexity int) int + __resolve_entities func(childComplexity int, representations []map[string]interface{}) int } User struct { @@ -229,6 +230,7 @@ type QueryResolver interface { AccountsListMembershipsForUser(ctx context.Context) ([]*entities.AccountMembership, error) AccountsListMembershipsForAccount(ctx context.Context, accountName string, role *types.Role) ([]*entities.AccountMembership, error) AccountsGetAccountMembership(ctx context.Context, accountName string) (*entities.AccountMembership, error) + AccountsEnsureKloudliteRegistryPullSecrets(ctx context.Context, accountName string) (bool, error) } type UserResolver interface { Accounts(ctx context.Context, obj *model.User) ([]*entities.AccountMembership, error) @@ -748,6 +750,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.AccountsCheckNameAvailability(childComplexity, args["name"].(string)), true + case "Query.accounts_ensureKloudliteRegistryPullSecrets": + if e.complexity.Query.AccountsEnsureKloudliteRegistryPullSecrets == nil { + break + } + + args, err := ec.field_Query_accounts_ensureKloudliteRegistryPullSecrets_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.AccountsEnsureKloudliteRegistryPullSecrets(childComplexity, args["accountName"].(string)), true + case "Query.accounts_getAccount": if e.complexity.Query.AccountsGetAccount == nil { break @@ -996,6 +1010,8 @@ type Query { accounts_listMembershipsForUser: [AccountMembership!] @isLoggedInAndVerified accounts_listMembershipsForAccount(accountName: String!, role: Github__com___kloudlite___api___apps___iam___types__Role): [AccountMembership!] @isLoggedInAndVerified accounts_getAccountMembership(accountName: String!): AccountMembership @isLoggedInAndVerified + + accounts_ensureKloudliteRegistryPullSecrets(accountName: String!): Boolean! @isLoggedInAndVerified } type Mutation { @@ -1492,6 +1508,21 @@ func (ec *executionContext) field_Query_accounts_checkNameAvailability_args(ctx return args, nil } +func (ec *executionContext) field_Query_accounts_ensureKloudliteRegistryPullSecrets_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["accountName"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("accountName")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["accountName"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_accounts_getAccountMembership_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -5596,6 +5627,81 @@ func (ec *executionContext) fieldContext_Query_accounts_getAccountMembership(ctx return fc, nil } +func (ec *executionContext) _Query_accounts_ensureKloudliteRegistryPullSecrets(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_accounts_ensureKloudliteRegistryPullSecrets(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().AccountsEnsureKloudliteRegistryPullSecrets(rctx, fc.Args["accountName"].(string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsLoggedInAndVerified == nil { + return nil, errors.New("directive isLoggedInAndVerified is not implemented") + } + return ec.directives.IsLoggedInAndVerified(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(bool); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be bool`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_accounts_ensureKloudliteRegistryPullSecrets(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_accounts_ensureKloudliteRegistryPullSecrets_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return + } + return fc, nil +} + func (ec *executionContext) _Query__entities(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query__entities(ctx, field) if err != nil { @@ -9038,6 +9144,29 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) } + out.Concurrently(i, func() graphql.Marshaler { + return rrm(innerCtx) + }) + case "accounts_ensureKloudliteRegistryPullSecrets": + field := field + + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_accounts_ensureKloudliteRegistryPullSecrets(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) + } + out.Concurrently(i, func() graphql.Marshaler { return rrm(innerCtx) }) diff --git a/apps/accounts/internal/app/graph/schema.graphqls b/apps/accounts/internal/app/graph/schema.graphqls index d3358a10b..5157bc45e 100644 --- a/apps/accounts/internal/app/graph/schema.graphqls +++ b/apps/accounts/internal/app/graph/schema.graphqls @@ -24,6 +24,8 @@ type Query { accounts_listMembershipsForUser: [AccountMembership!] @isLoggedInAndVerified accounts_listMembershipsForAccount(accountName: String!, role: Github__com___kloudlite___api___apps___iam___types__Role): [AccountMembership!] @isLoggedInAndVerified accounts_getAccountMembership(accountName: String!): AccountMembership @isLoggedInAndVerified + + accounts_ensureKloudliteRegistryPullSecrets(accountName: String!): Boolean! @isLoggedInAndVerified } type Mutation { diff --git a/apps/accounts/internal/app/graph/schema.resolvers.go b/apps/accounts/internal/app/graph/schema.resolvers.go index f07cf97d3..69fc0427f 100644 --- a/apps/accounts/internal/app/graph/schema.resolvers.go +++ b/apps/accounts/internal/app/graph/schema.resolvers.go @@ -6,12 +6,14 @@ package graph import ( "context" + + "github.com/kloudlite/api/pkg/errors" + "github.com/kloudlite/api/apps/accounts/internal/app/graph/generated" "github.com/kloudlite/api/apps/accounts/internal/app/graph/model" "github.com/kloudlite/api/apps/accounts/internal/domain" "github.com/kloudlite/api/apps/accounts/internal/entities" iamT "github.com/kloudlite/api/apps/iam/types" - "github.com/kloudlite/api/pkg/errors" "github.com/kloudlite/api/pkg/repos" ) @@ -250,6 +252,19 @@ func (r *queryResolver) AccountsGetAccountMembership(ctx context.Context, accoun return r.domain.GetAccountMembership(uc, accountName) } +// AccountsEnsureKloudliteRegistryPullSecrets is the resolver for the accounts_ensureKloudliteRegistryPullSecrets field. +func (r *queryResolver) AccountsEnsureKloudliteRegistryPullSecrets(ctx context.Context, accountName string) (bool, error) { + uc, err := toUserContext(ctx) + if err != nil { + return false, errors.NewE(err) + } + + if err := r.domain.EnsureKloudliteRegistryCredentials(uc, accountName); err != nil { + return false, err + } + return true, nil +} + // Accounts is the resolver for the accounts field. func (r *userResolver) Accounts(ctx context.Context, obj *model.User) ([]*entities.AccountMembership, error) { uc, err := toUserContext(ctx) @@ -279,6 +294,8 @@ func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } // User returns generated.UserResolver implementation. func (r *Resolver) User() generated.UserResolver { return &userResolver{r} } -type mutationResolver struct{ *Resolver } -type queryResolver struct{ *Resolver } -type userResolver struct{ *Resolver } +type ( + mutationResolver struct{ *Resolver } + queryResolver struct{ *Resolver } + userResolver struct{ *Resolver } +) diff --git a/apps/accounts/internal/domain/accounts.go b/apps/accounts/internal/domain/accounts.go index cdd2f5db0..651b44799 100644 --- a/apps/accounts/internal/domain/accounts.go +++ b/apps/accounts/internal/domain/accounts.go @@ -3,15 +3,17 @@ package domain import ( "context" "fmt" + "strings" + fc "github.com/kloudlite/api/apps/accounts/internal/entities/field-constants" "github.com/kloudlite/api/common/fields" "github.com/kloudlite/api/pkg/errors" - "strings" "github.com/kloudlite/api/apps/accounts/internal/entities" iamT "github.com/kloudlite/api/apps/iam/types" "github.com/kloudlite/api/common" "github.com/kloudlite/api/constants" + "github.com/kloudlite/api/grpc-interfaces/container_registry" "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/iam" fn "github.com/kloudlite/api/pkg/functions" "github.com/kloudlite/api/pkg/repos" @@ -92,8 +94,61 @@ func (d *domain) deleteNamespaceForAccount(ctx context.Context, targetNamespace panic("not implemented. Yet to decide if we want to delete namespace when account is deleted") } +func (d *domain) ensureKloudliteRegistryCredentials(ctx UserContext, account *entities.Account) error { + credentialsName := "kloudlite-registry-pull-creds" + + secret := &corev1.Secret{} + if err := d.k8sClient.Get(ctx, fn.NN(account.TargetNamespace, credentialsName), secret); err != nil { + secret = nil + } + + if secret != nil { + d.logger.Infof("kloudlite registry image pull secret already exists") + return nil + } + + out, err := d.containerRegistryClient.CreateReadOnlyCredential(ctx, &container_registry.CreateReadOnlyCredentialIn{ + AccountName: account.Name, + UserId: string(ctx.UserId), + CredentialName: credentialsName, + RegistryUsername: fmt.Sprintf("account_%s", account.Name), + }) + if err != nil { + return err + } + + if err := d.k8sClient.Create(ctx, &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: credentialsName, + Namespace: account.TargetNamespace, + }, + Immutable: new(bool), + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: []byte(out.DockerConfigJson), + }, + Type: corev1.SecretTypeDockerConfigJson, + }); err != nil { + return err + } + + return nil +} + +func (d *domain) EnsureKloudliteRegistryCredentials(ctx UserContext, accountName string) error { + a, err := d.findAccount(ctx, accountName) + if err != nil { + return errors.NewE(err) + } + + return d.ensureKloudliteRegistryCredentials(ctx, a) +} + func (d *domain) CreateAccount(ctx UserContext, account entities.Account) (*entities.Account, error) { - account.TargetNamespace = fmt.Sprintf("kl-account-%s", account.Name) + account.TargetNamespace = constants.GetAccountTargetNamespace(account.Name) account.IsActive = fn.New(true) account.CreatedBy = common.CreatedOrUpdatedBy{ UserId: ctx.UserId, @@ -115,6 +170,10 @@ func (d *domain) CreateAccount(ctx UserContext, account entities.Account) (*enti return nil, errors.NewE(err) } + if err := d.ensureKloudliteRegistryCredentials(ctx, &account); err != nil { + return nil, errors.NewE(err) + } + return acc, nil } @@ -147,7 +206,6 @@ func (d *domain) UpdateAccount(ctx UserContext, accountIn entities.Account) (*en UserEmail: ctx.UserEmail, }, }) - if err != nil { return nil, errors.NewE(err) } diff --git a/apps/accounts/internal/domain/domain.go b/apps/accounts/internal/domain/domain.go index 928512927..8ea5da489 100644 --- a/apps/accounts/internal/domain/domain.go +++ b/apps/accounts/internal/domain/domain.go @@ -3,10 +3,10 @@ package domain import ( "github.com/kloudlite/api/apps/accounts/internal/entities" iamT "github.com/kloudlite/api/apps/iam/types" + "github.com/kloudlite/api/grpc-interfaces/container_registry" "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/auth" "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/comms" "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/console" - "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/container_registry" "github.com/kloudlite/api/grpc-interfaces/kloudlite.io/rpc/iam" "github.com/kloudlite/api/pkg/k8s" "github.com/kloudlite/api/pkg/logging" @@ -34,6 +34,8 @@ type AccountService interface { ActivateAccount(ctx UserContext, name string) (bool, error) DeactivateAccount(ctx UserContext, name string) (bool, error) + + EnsureKloudliteRegistryCredentials(ctx UserContext, accountName string) error } type InvitationService interface { @@ -86,7 +88,7 @@ type domain struct { func NewDomain( iamCli iam.IAMClient, consoleClient console.ConsoleClient, - // containerRegistryClient container_registry.ContainerRegistryClient, + containerRegistryClient container_registry.ContainerRegistryClient, authClient auth.AuthClient, commsClient comms.CommsClient, @@ -99,16 +101,16 @@ func NewDomain( logger logging.Logger, ) Domain { return &domain{ - authClient: authClient, - iamClient: iamCli, - consoleClient: consoleClient, - commsClient: commsClient, + authClient: authClient, + iamClient: iamCli, + consoleClient: consoleClient, + commsClient: commsClient, + containerRegistryClient: containerRegistryClient, k8sClient: k8sClient, accountRepo: accountRepo, invitationRepo: invitationRepo, - // accountInviteTokenRepo: accountInviteTokenRepo, logger: logger, } diff --git a/apps/accounts/internal/env/env.go b/apps/accounts/internal/env/env.go index 71f1bcfe1..f3c90dc06 100644 --- a/apps/accounts/internal/env/env.go +++ b/apps/accounts/internal/env/env.go @@ -20,6 +20,7 @@ type Env struct { ContainerRegistryGrpcAddr string `env:"CONTAINER_REGISTRY_GRPC_ADDR" required:"true"` ConsoleGrpcAddr string `env:"CONSOLE_GRPC_ADDR" required:"true"` AuthGrpcAddr string `env:"AUTH_GRPC_ADDR" required:"true"` + SessionKVBucket string `env:"SESSION_KV_BUCKET" required:"true"` NatsURL string `env:"NATS_URL" required:"true"` IsDev bool diff --git a/apps/accounts/main.go b/apps/accounts/main.go index e41888f96..b127a7f01 100644 --- a/apps/accounts/main.go +++ b/apps/accounts/main.go @@ -3,10 +3,11 @@ package main import ( "context" "flag" - "github.com/kloudlite/api/pkg/errors" "os" "time" + "github.com/kloudlite/api/pkg/errors" + "github.com/kloudlite/api/common" "github.com/kloudlite/api/pkg/k8s" "github.com/kloudlite/api/pkg/logging" diff --git a/apps/console/internal/domain/environment.go b/apps/console/internal/domain/environment.go index 9f290ac6a..857240e3a 100644 --- a/apps/console/internal/domain/environment.go +++ b/apps/console/internal/domain/environment.go @@ -161,13 +161,48 @@ func (d *domain) CreateEnvironment(ctx ConsoleContext, projectName string, env e d.logger.Errorf(err, "error while adding membership") } + if err := d.applyEnvironmentTargetNamespace(ctx, nenv); err != nil { + return nil, errors.NewE(err) + } + if err := d.applyK8sResource(ctx, nenv.ProjectName, &nenv.Environment, nenv.RecordVersion); err != nil { return nil, errors.NewE(err) } + if err := d.syncAccountLevelImagePullSecrets(ctx, nenv.ProjectName, nenv.Spec.TargetNamespace); err != nil { + return nil, errors.NewE(err) + } + return nenv, nil } +func (d *domain) syncAccountLevelImagePullSecrets(ctx ConsoleContext, projectName string, envTargetNamespace string) error { + secrets, err := d.k8sClient.ListSecrets(ctx, constants.GetAccountTargetNamespace(ctx.AccountName), corev1.SecretTypeDockerConfigJson) + if err != nil { + return err + } + + for i := range secrets { + if err := d.applyK8sResource(ctx, projectName, &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: secrets[i].Name, + Namespace: envTargetNamespace, + }, + Data: secrets[i].Data, + StringData: secrets[i].StringData, + Type: secrets[i].Type, + }, 1); err != nil { + return err + } + } + + return nil +} + func (d *domain) CloneEnvironment(ctx ConsoleContext, projectName string, sourceEnvName string, destinationEnvName string, displayName string, envRoutingMode crdsv1.EnvironmentRoutingMode) (*entities.Environment, error) { if err := d.canMutateResourcesInProject(ctx, projectName); err != nil { return nil, errors.NewE(err) @@ -242,6 +277,10 @@ func (d *domain) CloneEnvironment(ctx ConsoleContext, projectName string, source return nil, errors.NewE(err) } + if err := d.syncAccountLevelImagePullSecrets(ctx, destEnv.ProjectName, destEnv.Spec.TargetNamespace); err != nil { + return nil, errors.NewE(err) + } + if err := d.applyK8sResource(ctx, sourceEnv.ProjectName, &destEnv.Environment, destEnv.RecordVersion); err != nil { return nil, errors.NewE(err) } @@ -564,6 +603,22 @@ func (d *domain) OnEnvironmentUpdateMessage(ctx ConsoleContext, env entities.Env return nil } +func (d *domain) applyEnvironmentTargetNamespace(ctx ConsoleContext, env *entities.Environment) error { + if err := d.applyK8sResource(ctx, env.ProjectName, &corev1.Namespace{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Namespace"}, + ObjectMeta: metav1.ObjectMeta{ + Name: env.Spec.TargetNamespace, + Labels: map[string]string{ + constants.EnvNameKey: env.Name, + }, + }, + }, env.RecordVersion); err != nil { + return errors.NewE(err) + } + + return nil +} + func (d *domain) ResyncEnvironment(ctx ConsoleContext, projectName string, name string) error { if err := d.canMutateResourcesInProject(ctx, projectName); err != nil { return errors.NewE(err) diff --git a/apps/console/internal/domain/image-pull-secret.go b/apps/console/internal/domain/image-pull-secret.go index ba1f25ba4..4689fd0f1 100644 --- a/apps/console/internal/domain/image-pull-secret.go +++ b/apps/console/internal/domain/image-pull-secret.go @@ -3,6 +3,7 @@ package domain import ( "encoding/base64" "encoding/json" + "github.com/kloudlite/api/apps/console/internal/entities" fc "github.com/kloudlite/api/apps/console/internal/entities/field-constants" "github.com/kloudlite/api/common" @@ -27,7 +28,6 @@ func (d *domain) ListImagePullSecrets(ctx ResourceContext, search map[string]rep } func (d *domain) findImagePullSecret(ctx ResourceContext, name string) (*entities.ImagePullSecret, error) { - ips, err := d.pullSecretsRepo.FindOne( ctx, ctx.DBFilters().Add(fields.MetadataName, name), @@ -254,7 +254,6 @@ func (d *domain) OnImagePullSecretUpdateMessage(ctx ResourceContext, ips entitie common.PatchForSyncFromAgent(&ips, recordVersion, status, common.PatchOpts{ MessageTimestamp: opts.MessageTimestamp, })) - if err != nil { return err } diff --git a/apps/container-registry/Containerfile b/apps/container-registry/Containerfile index 159536c02..753dd24ea 100644 --- a/apps/container-registry/Containerfile +++ b/apps/container-registry/Containerfile @@ -1,4 +1,5 @@ -FROM gcr.io/distroless/static-debian12:nonroot +# FROM gcr.io/distroless/static-debian12:nonroot +FROM cgr.dev/chainguard/static ARG BINARY COPY ./bin/${BINARY} ./container-registry ENTRYPOINT ["./container-registry"] diff --git a/apps/container-registry/internal/app/grpc-server.go b/apps/container-registry/internal/app/grpc-server.go new file mode 100644 index 000000000..f6edabc1e --- /dev/null +++ b/apps/container-registry/internal/app/grpc-server.go @@ -0,0 +1,70 @@ +package app + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/kloudlite/api/apps/container-registry/internal/domain" + "github.com/kloudlite/api/apps/container-registry/internal/domain/entities" + "github.com/kloudlite/api/apps/container-registry/internal/env" + "github.com/kloudlite/api/grpc-interfaces/container_registry" + "github.com/kloudlite/api/pkg/grpc" + "github.com/kloudlite/api/pkg/repos" +) + +type ContainerRegistryGRPCServer grpc.Server + +type grpcServer struct { + container_registry.UnimplementedContainerRegistryServer + d domain.Domain + ev *env.Env +} + +// CreateReadOnlyCredentials implements container_registry.ContainerRegistryServer. +func (g *grpcServer) CreateReadOnlyCredential(ctx context.Context, in *container_registry.CreateReadOnlyCredentialIn) (*container_registry.CreateReadOnlyCredentialOut, error) { + regctx := domain.RegistryContext{ + Context: ctx, + UserId: repos.ID(in.UserId), + UserName: "created-by-kloudlite", + AccountName: in.AccountName, + UserEmail: "", + } + + creds, err := g.d.CreateCredential(regctx, entities.Credential{ + AccountName: in.AccountName, + Access: entities.RepoAccessReadOnly, + Expiration: entities.Expiration{Unit: entities.ExpirationUnitYear, Value: 17}, + Name: in.CredentialName, + UserName: in.RegistryUsername, + }) + if err != nil { + return nil, err + } + + token, err := g.d.GetToken(regctx, in.RegistryUsername) + if err != nil { + return nil, err + } + + dockerConfigJson, err := json.Marshal(map[string]any{ + "auths": map[string]any{ + g.ev.RegistryHost: map[string]any{ + "auth": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", creds.UserName, token))), + }, + }, + }) + if err != nil { + return nil, err + } + + return &container_registry.CreateReadOnlyCredentialOut{ + DockerConfigJson: dockerConfigJson, + }, nil +} + +func InitializeGrpcServer(server ContainerRegistryGRPCServer, d domain.Domain, ev *env.Env) { + gs := grpcServer{d: d, ev: ev} + container_registry.RegisterContainerRegistryServer(server, &gs) +} diff --git a/apps/container-registry/internal/app/main.go b/apps/container-registry/internal/app/main.go index 3817327b9..f874b77c8 100644 --- a/apps/container-registry/internal/app/main.go +++ b/apps/container-registry/internal/app/main.go @@ -117,7 +117,7 @@ var Module = fx.Module("app", ConsumerConfig: msg_nats.ConsumerConfig{ Name: consumerName, Durable: consumerName, - Description: "this consumer receives container registry resource updates, processes them, and keeps our Database updated about things happening in the cluster", + Description: "this consumer receives container registry resource updates, processes them, and keeps our Database updated about things happening in our client's cluster", FilterSubjects: []string{topic}, }, }) @@ -241,7 +241,6 @@ var Module = fx.Module("app", ), fx.Invoke(func(server AuthorizerHttpServer, envs *env.Env, d domain.Domain, logger logging.Logger) { - authLogger := logger.WithKV("route", "/auth") a := server.Raw() @@ -316,4 +315,6 @@ var Module = fx.Module("app", }), domain.Module, + + fx.Invoke(InitializeGrpcServer), ) diff --git a/apps/container-registry/internal/domain/api.go b/apps/container-registry/internal/domain/api.go index 2bbf95bc0..78c33a9b8 100644 --- a/apps/container-registry/internal/domain/api.go +++ b/apps/container-registry/internal/domain/api.go @@ -26,6 +26,7 @@ type Domain interface { ProcessRegistryEvents(ctx context.Context, events []entities.Event, logger logging.Logger) error CheckUserNameAvailability(ctx RegistryContext, username string) (*CheckNameAvailabilityOutput, error) + // registry ListRepositories(ctx RegistryContext, search map[string]repos.MatchFilter, pagination repos.CursorPagination) (*repos.PaginatedRecord[*entities.Repository], error) CreateRepository(ctx RegistryContext, repoName string) (*entities.Repository, error) @@ -81,7 +82,7 @@ type Domain interface { GetBuildRun(ctx RegistryContext, repoName string, runName string) (*entities.BuildRun, error) OnBuildRunUpdateMessage(ctx RegistryContext, buildRun entities.BuildRun) error OnBuildRunDeleteMessage(ctx RegistryContext, buildRun entities.BuildRun) error - OnBuildRunApplyErrorMessage(ctx RegistryContext, clusterName string,name string, errorMsg string) error + OnBuildRunApplyErrorMessage(ctx RegistryContext, clusterName string, name string, errorMsg string) error ListBuildsByCache(ctx RegistryContext, cacheId repos.ID, pagination repos.CursorPagination) (*repos.PaginatedRecord[*entities.Build], error) CreateBuildRun(ctx RegistryContext, build *entities.Build, hook *GitWebhookPayload, pullToken string) error } diff --git a/apps/container-registry/internal/domain/credential.go b/apps/container-registry/internal/domain/credential.go index b47a5a226..6635c7516 100644 --- a/apps/container-registry/internal/domain/credential.go +++ b/apps/container-registry/internal/domain/credential.go @@ -3,11 +3,12 @@ package domain import ( "context" "fmt" - fc "github.com/kloudlite/api/apps/container-registry/internal/domain/entities/field-constants" - "github.com/kloudlite/api/common/fields" "regexp" "time" + fc "github.com/kloudlite/api/apps/container-registry/internal/domain/entities/field-constants" + "github.com/kloudlite/api/common/fields" + "github.com/kloudlite/api/pkg/errors" "github.com/kloudlite/api/apps/container-registry/internal/domain/entities" @@ -144,14 +145,13 @@ func (d *Impl) CheckUserNameAvailability(ctx RegistryContext, username string) ( }, nil } +// var reCredsUsername = regexp.MustCompile(`^([a-z])[a-z0-9_]+$`) +var reCredsUsername = regexp.MustCompile(`^([a-z])[a-z0-9_-]+$`) + // CreateCredential implements Domain. func (d *Impl) CreateCredential(ctx RegistryContext, credential entities.Credential) (*entities.Credential, error) { - pattern := `^([a-z])[a-z0-9_]+$` - - re := regexp.MustCompile(pattern) - - if !re.MatchString(credential.UserName) { - return nil, errors.Newf("invalid credential name, must be lowercase alphanumeric with underscore") + if !reCredsUsername.MatchString(credential.UserName) { + return nil, errors.Newf("invalid credential user name, must be lowercase alphanumeric with underscore") } if credential.UserName == KL_ADMIN { diff --git a/apps/container-registry/internal/domain/main.go b/apps/container-registry/internal/domain/main.go index 879f5d4ef..69de4a01c 100644 --- a/apps/container-registry/internal/domain/main.go +++ b/apps/container-registry/internal/domain/main.go @@ -3,11 +3,13 @@ package domain import ( "context" "fmt" - fc "github.com/kloudlite/api/apps/container-registry/internal/domain/entities/field-constants" - "github.com/kloudlite/api/common/fields" + "net/http" "regexp" "strings" + fc "github.com/kloudlite/api/apps/container-registry/internal/domain/entities/field-constants" + "github.com/kloudlite/api/common/fields" + "github.com/kloudlite/api/pkg/errors" "github.com/kloudlite/api/apps/container-registry/internal/domain/entities" @@ -42,22 +44,14 @@ type Impl struct { dispatcher ResourceDispatcher } +var repositoryNamePattern = regexp.MustCompile(`.*[^\/].*\/.*$`) + func (d *Impl) ProcessRegistryEvents(ctx context.Context, events []entities.Event, logger logging.Logger) error { l := logger.WithName("registry-event") - pattern := `.*[^\/].*\/.*$` - - re, err := regexp.Compile(pattern) - if err != nil { - l.Errorf(err) - return errors.NewE(err) - } - for _, e := range events { - r := e.Target.Repository - - if !re.MatchString(r) { + if !repositoryNamePattern.MatchString(r) { l.Warnf("invalid repository name %s\n, ignoring", r) return nil } @@ -69,13 +63,13 @@ func (d *Impl) ProcessRegistryEvents(ctx context.Context, events []entities.Even tag := e.Target.Tag switch e.Request.Method { - case "PUT": - + case http.MethodPut: if tag == "" { fmt.Println("tag is empty with digest", e.Target.Digest) return nil } + // INFO: finding by tag digest, err := d.digestRepo.FindOne(ctx, repos.Filter{ fc.DigestTags: map[string]any{ "$in": []string{tag}, @@ -87,19 +81,10 @@ func (d *Impl) ProcessRegistryEvents(ctx context.Context, events []entities.Even return errors.NewE(err) } - if digest == nil { - } else { - digest.Tags = func() []string { - tags := []string{} - - for _, v := range digest.Tags { - if v != tag { - tags = append(tags, v) - } - } - - return tags - }() + if digest != nil { + digest.Tags = slices.Filter(nil, digest.Tags, func(s string) bool { + return s != tag + }) if len(digest.Tags) == 0 { if err := d.digestRepo.DeleteById(ctx, digest.Id); err != nil { @@ -111,15 +96,14 @@ func (d *Impl) ProcessRegistryEvents(ctx context.Context, events []entities.Even return errors.NewE(err) } } - } + // INFO: finding by digest digest, err = d.digestRepo.FindOne(ctx, repos.Filter{ fc.DigestDigest: e.Target.Digest, fc.DigestRepository: repoName, fields.AccountName: accountName, }) - if err != nil { return errors.NewE(err) } @@ -153,32 +137,20 @@ func (d *Impl) ProcessRegistryEvents(ctx context.Context, events []entities.Even } } - ee, err := d.repositoryRepo.FindOne(ctx, repos.Filter{ + if _, err := d.repositoryRepo.Upsert(ctx, repos.Filter{ fields.AccountName: accountName, fc.RepositoryName: repoName, - }) - if err != nil { - return errors.NewE(err) - } - - if ee == nil { - _, err := d.repositoryRepo.Create(ctx, &entities.Repository{}) - if err != nil { - return errors.NewE(err) - } - return nil - } - - ee.LastUpdatedBy = common.CreatedOrUpdatedBy{ - UserName: e.Actor.Name, - } - - if _, err := d.repositoryRepo.UpdateById(ctx, ee.Id, ee); err != nil { + }, &entities.Repository{ + CreatedBy: common.CreatedOrUpdatedBy{UserName: e.Actor.Name}, + LastUpdatedBy: common.CreatedOrUpdatedBy{UserName: e.Actor.Name}, + AccountName: accountName, + Name: repoName, + }); err != nil { d.logger.Errorf(err) + return errors.NewE(err) } - case "DELETE": - + case http.MethodDelete: l.Infof("DELETE %s:%s %s", e.Target.Repository, e.Target.Tag, e.Target.Digest) if err := d.digestRepo.DeleteOne(ctx, repos.Filter{ @@ -190,10 +162,10 @@ func (d *Impl) ProcessRegistryEvents(ctx context.Context, events []entities.Even return errors.NewE(err) } - case "HEAD": + case http.MethodHead: l.Infof("HEAD %s:%s", e.Target.Repository, e.Target.Tag) - case "GET": + case http.MethodGet: l.Infof("GET %s:%s", e.Target.Repository, e.Target.Tag) default: diff --git a/apps/container-registry/internal/env/env.go b/apps/container-registry/internal/env/env.go index 30e803b76..d58d6d2a2 100644 --- a/apps/container-registry/internal/env/env.go +++ b/apps/container-registry/internal/env/env.go @@ -7,6 +7,7 @@ import ( type Env struct { Port uint16 `env:"PORT" required:"true"` + GrpcPort uint16 `env:"GRPC_PORT" required:"true"` CookieDomain string `env:"COOKIE_DOMAIN" required:"true"` AccountCookieName string `env:"ACCOUNT_COOKIE_NAME" required:"true"` diff --git a/apps/container-registry/internal/framework/framework.go b/apps/container-registry/internal/framework/framework.go index f806c8102..a2f455b0d 100644 --- a/apps/container-registry/internal/framework/framework.go +++ b/apps/container-registry/internal/framework/framework.go @@ -3,12 +3,14 @@ package framework import ( "context" "fmt" + "time" + "github.com/kloudlite/api/common" "github.com/kloudlite/api/pkg/nats" app "github.com/kloudlite/api/apps/container-registry/internal/app" "github.com/kloudlite/api/apps/container-registry/internal/env" - rpc "github.com/kloudlite/api/pkg/grpc" + "github.com/kloudlite/api/pkg/grpc" httpServer "github.com/kloudlite/api/pkg/http-server" "github.com/kloudlite/api/pkg/kv" "github.com/kloudlite/api/pkg/logging" @@ -38,11 +40,11 @@ var Module = fx.Module("framework", }), fx.Provide(func(ev *env.Env) (app.IAMGrpcClient, error) { - return rpc.NewGrpcClient(ev.IAMGrpcAddr) + return grpc.NewGrpcClient(ev.IAMGrpcAddr) }), fx.Provide(func(ev *env.Env) (app.AuthGrpcClient, error) { - return rpc.NewGrpcClient(ev.AuthGrpcAddr) + return grpc.NewGrpcClient(ev.AuthGrpcAddr) }), mongoDb.NewMongoClientFx[*fm](), @@ -102,4 +104,43 @@ var Module = fx.Module("framework", }, }) }), + + // creates New GRPC server + fx.Provide(func(logger logging.Logger) (app.ContainerRegistryGRPCServer, error) { + return grpc.NewGrpcServer(grpc.ServerOpts{ + Logger: logger.WithName("GRPC server"), + }) + }), + + // handles GRPC server lifecycle + fx.Invoke(func(lf fx.Lifecycle, server app.ContainerRegistryGRPCServer, ev *env.Env, logger logging.Logger) { + lf.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + errCh := make(chan error, 1) + + tctx, cf := context.WithTimeout(ctx, 2*time.Second) + defer cf() + + go func() { + err := server.Listen(fmt.Sprintf(":%d", ev.GrpcPort)) + if err != nil { + errCh <- err + logger.Errorf(err, "failed to start grpc server") + } + }() + + select { + case <-tctx.Done(): + case err := <-errCh: + return err + } + + return nil + }, + OnStop: func(context.Context) error { + server.Stop() + return nil + }, + }) + }), ) diff --git a/constants/constants.go b/constants/constants.go index 2d1d6e59b..e29927efd 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -1,5 +1,7 @@ package constants +import "fmt" + type ResourceType string const ( @@ -156,3 +158,7 @@ const ( ManagedByKloudlite string = "kloudlite.io/managed-by.kloudlite" ) + +func GetAccountTargetNamespace(accountName string) string { + return fmt.Sprintf("kl-account-%s", accountName) +} diff --git a/grpc-interfaces/container-registry.proto b/grpc-interfaces/container-registry.proto index ad01eb11b..6fae4458f 100644 --- a/grpc-interfaces/container-registry.proto +++ b/grpc-interfaces/container-registry.proto @@ -1,25 +1,40 @@ syntax = "proto3"; -option go_package = "kloudlite.io/rpc/container_registry"; +// option go_package = "kloudlite.io/rpc/container_registry"; +option go_package = "./container_registry"; service ContainerRegistry { - rpc CreateProjectForAccount(CreateProjectIn) returns (CreateProjectOut); - rpc GetSvcCredentials(GetSvcCredentialsIn) returns (GetSvcCredentialsOut); + // rpc CreateProjectForAccount(CreateProjectIn) returns (CreateProjectOut); + // rpc GetSvcCredentials(GetSvcCredentialsIn) returns (GetSvcCredentialsOut); + rpc CreateReadOnlyCredential(CreateReadOnlyCredentialIn) returns (CreateReadOnlyCredentialOut); } -message CreateProjectIn { - string accountName = 1; -} +// message CreateProjectIn { +// string accountName = 1; +// } +// +// message CreateProjectOut { +// bool success = 1; +// } +// +// message GetSvcCredentialsIn { +// string accountName = 1; +// } +// +// message GetSvcCredentialsOut { +// string userName = 1; +// string password = 2; +// } -message CreateProjectOut { - bool success = 1; -} - -message GetSvcCredentialsIn { +message CreateReadOnlyCredentialIn { string accountName = 1; + string userId = 2; + + string credentialName = 3; + string registryUsername = 4; } -message GetSvcCredentialsOut { - string userName = 1; - string password = 2; +message CreateReadOnlyCredentialOut { + // dcokerconfigjson is as per format: https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets + bytes dockerConfigJson = 1; } diff --git a/grpc-interfaces/container_registry/container-registry.pb.go b/grpc-interfaces/container_registry/container-registry.pb.go new file mode 100644 index 000000000..70749cf56 --- /dev/null +++ b/grpc-interfaces/container_registry/container-registry.pb.go @@ -0,0 +1,249 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.24.4 +// source: container-registry.proto + +package container_registry + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CreateReadOnlyCredentialIn struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AccountName string `protobuf:"bytes,1,opt,name=accountName,proto3" json:"accountName,omitempty"` + UserId string `protobuf:"bytes,2,opt,name=userId,proto3" json:"userId,omitempty"` + CredentialName string `protobuf:"bytes,3,opt,name=credentialName,proto3" json:"credentialName,omitempty"` + RegistryUsername string `protobuf:"bytes,4,opt,name=registryUsername,proto3" json:"registryUsername,omitempty"` +} + +func (x *CreateReadOnlyCredentialIn) Reset() { + *x = CreateReadOnlyCredentialIn{} + if protoimpl.UnsafeEnabled { + mi := &file_container_registry_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateReadOnlyCredentialIn) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateReadOnlyCredentialIn) ProtoMessage() {} + +func (x *CreateReadOnlyCredentialIn) ProtoReflect() protoreflect.Message { + mi := &file_container_registry_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateReadOnlyCredentialIn.ProtoReflect.Descriptor instead. +func (*CreateReadOnlyCredentialIn) Descriptor() ([]byte, []int) { + return file_container_registry_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateReadOnlyCredentialIn) GetAccountName() string { + if x != nil { + return x.AccountName + } + return "" +} + +func (x *CreateReadOnlyCredentialIn) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *CreateReadOnlyCredentialIn) GetCredentialName() string { + if x != nil { + return x.CredentialName + } + return "" +} + +func (x *CreateReadOnlyCredentialIn) GetRegistryUsername() string { + if x != nil { + return x.RegistryUsername + } + return "" +} + +type CreateReadOnlyCredentialOut struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // dcokerconfigjson is as per format: https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets + DockerConfigJson []byte `protobuf:"bytes,1,opt,name=dockerConfigJson,proto3" json:"dockerConfigJson,omitempty"` +} + +func (x *CreateReadOnlyCredentialOut) Reset() { + *x = CreateReadOnlyCredentialOut{} + if protoimpl.UnsafeEnabled { + mi := &file_container_registry_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateReadOnlyCredentialOut) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateReadOnlyCredentialOut) ProtoMessage() {} + +func (x *CreateReadOnlyCredentialOut) ProtoReflect() protoreflect.Message { + mi := &file_container_registry_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateReadOnlyCredentialOut.ProtoReflect.Descriptor instead. +func (*CreateReadOnlyCredentialOut) Descriptor() ([]byte, []int) { + return file_container_registry_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateReadOnlyCredentialOut) GetDockerConfigJson() []byte { + if x != nil { + return x.DockerConfigJson + } + return nil +} + +var File_container_registry_proto protoreflect.FileDescriptor + +var file_container_registry_proto_rawDesc = []byte{ + 0x0a, 0x18, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2d, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xaa, 0x01, 0x0a, 0x1a, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x43, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x55, + 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x4f, 0x75, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x10, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x73, + 0x6f, 0x6e, 0x32, 0x6a, 0x0a, 0x11, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x12, 0x55, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x61, 0x64, + 0x4f, 0x6e, 0x6c, 0x79, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x6e, + 0x1a, 0x1c, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, + 0x79, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4f, 0x75, 0x74, 0x42, 0x16, + 0x5a, 0x14, 0x2e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_container_registry_proto_rawDescOnce sync.Once + file_container_registry_proto_rawDescData = file_container_registry_proto_rawDesc +) + +func file_container_registry_proto_rawDescGZIP() []byte { + file_container_registry_proto_rawDescOnce.Do(func() { + file_container_registry_proto_rawDescData = protoimpl.X.CompressGZIP(file_container_registry_proto_rawDescData) + }) + return file_container_registry_proto_rawDescData +} + +var file_container_registry_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_container_registry_proto_goTypes = []interface{}{ + (*CreateReadOnlyCredentialIn)(nil), // 0: CreateReadOnlyCredentialIn + (*CreateReadOnlyCredentialOut)(nil), // 1: CreateReadOnlyCredentialOut +} +var file_container_registry_proto_depIdxs = []int32{ + 0, // 0: ContainerRegistry.CreateReadOnlyCredential:input_type -> CreateReadOnlyCredentialIn + 1, // 1: ContainerRegistry.CreateReadOnlyCredential:output_type -> CreateReadOnlyCredentialOut + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_container_registry_proto_init() } +func file_container_registry_proto_init() { + if File_container_registry_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_container_registry_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateReadOnlyCredentialIn); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_container_registry_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateReadOnlyCredentialOut); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_container_registry_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_container_registry_proto_goTypes, + DependencyIndexes: file_container_registry_proto_depIdxs, + MessageInfos: file_container_registry_proto_msgTypes, + }.Build() + File_container_registry_proto = out.File + file_container_registry_proto_rawDesc = nil + file_container_registry_proto_goTypes = nil + file_container_registry_proto_depIdxs = nil +} diff --git a/grpc-interfaces/container_registry/container-registry_grpc.pb.go b/grpc-interfaces/container_registry/container-registry_grpc.pb.go new file mode 100644 index 000000000..5c0d88f1b --- /dev/null +++ b/grpc-interfaces/container_registry/container-registry_grpc.pb.go @@ -0,0 +1,113 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v4.24.4 +// source: container-registry.proto + +package container_registry + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + ContainerRegistry_CreateReadOnlyCredential_FullMethodName = "/ContainerRegistry/CreateReadOnlyCredential" +) + +// ContainerRegistryClient is the client API for ContainerRegistry service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ContainerRegistryClient interface { + // rpc CreateProjectForAccount(CreateProjectIn) returns (CreateProjectOut); + // rpc GetSvcCredentials(GetSvcCredentialsIn) returns (GetSvcCredentialsOut); + CreateReadOnlyCredential(ctx context.Context, in *CreateReadOnlyCredentialIn, opts ...grpc.CallOption) (*CreateReadOnlyCredentialOut, error) +} + +type containerRegistryClient struct { + cc grpc.ClientConnInterface +} + +func NewContainerRegistryClient(cc grpc.ClientConnInterface) ContainerRegistryClient { + return &containerRegistryClient{cc} +} + +func (c *containerRegistryClient) CreateReadOnlyCredential(ctx context.Context, in *CreateReadOnlyCredentialIn, opts ...grpc.CallOption) (*CreateReadOnlyCredentialOut, error) { + out := new(CreateReadOnlyCredentialOut) + err := c.cc.Invoke(ctx, ContainerRegistry_CreateReadOnlyCredential_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ContainerRegistryServer is the server API for ContainerRegistry service. +// All implementations must embed UnimplementedContainerRegistryServer +// for forward compatibility +type ContainerRegistryServer interface { + // rpc CreateProjectForAccount(CreateProjectIn) returns (CreateProjectOut); + // rpc GetSvcCredentials(GetSvcCredentialsIn) returns (GetSvcCredentialsOut); + CreateReadOnlyCredential(context.Context, *CreateReadOnlyCredentialIn) (*CreateReadOnlyCredentialOut, error) + mustEmbedUnimplementedContainerRegistryServer() +} + +// UnimplementedContainerRegistryServer must be embedded to have forward compatible implementations. +type UnimplementedContainerRegistryServer struct { +} + +func (UnimplementedContainerRegistryServer) CreateReadOnlyCredential(context.Context, *CreateReadOnlyCredentialIn) (*CreateReadOnlyCredentialOut, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateReadOnlyCredential not implemented") +} +func (UnimplementedContainerRegistryServer) mustEmbedUnimplementedContainerRegistryServer() {} + +// UnsafeContainerRegistryServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ContainerRegistryServer will +// result in compilation errors. +type UnsafeContainerRegistryServer interface { + mustEmbedUnimplementedContainerRegistryServer() +} + +func RegisterContainerRegistryServer(s grpc.ServiceRegistrar, srv ContainerRegistryServer) { + s.RegisterService(&ContainerRegistry_ServiceDesc, srv) +} + +func _ContainerRegistry_CreateReadOnlyCredential_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateReadOnlyCredentialIn) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ContainerRegistryServer).CreateReadOnlyCredential(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ContainerRegistry_CreateReadOnlyCredential_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ContainerRegistryServer).CreateReadOnlyCredential(ctx, req.(*CreateReadOnlyCredentialIn)) + } + return interceptor(ctx, in, info, handler) +} + +// ContainerRegistry_ServiceDesc is the grpc.ServiceDesc for ContainerRegistry service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ContainerRegistry_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ContainerRegistry", + HandlerType: (*ContainerRegistryServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateReadOnlyCredential", + Handler: _ContainerRegistry_CreateReadOnlyCredential_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "container-registry.proto", +} diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go index bc573ec20..440be9a83 100644 --- a/pkg/k8s/client.go +++ b/pkg/k8s/client.go @@ -9,6 +9,7 @@ import ( fn "github.com/kloudlite/api/pkg/functions" "github.com/kloudlite/operator/pkg/kubectl" "github.com/xeipuuv/gojsonschema" + corev1 "k8s.io/api/core/v1" apiExtensionsV1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -26,6 +27,8 @@ type Client interface { Update(ctx context.Context, obj client.Object) error Delete(ctx context.Context, obj client.Object) error + ListSecrets(ctx context.Context, namespace string, secretType corev1.SecretType) ([]corev1.Secret, error) + // custom ones ValidateObject(ctx context.Context, obj client.Object) error @@ -39,6 +42,18 @@ type clientHandler struct { yamlclient kubectl.YAMLClient } +// ListSecrets implements Client. +func (ch *clientHandler) ListSecrets(ctx context.Context, namespace string, secretType corev1.SecretType) ([]corev1.Secret, error) { + out, err := ch.yamlclient.Client().CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{ + FieldSelector: fmt.Sprintf("type=%s", secretType), + }) + if err != nil { + return nil, err + } + + return out.Items, nil +} + // Delete implements Client. func (ch *clientHandler) Delete(ctx context.Context, obj client.Object) error { return ch.kclient.Delete(ctx, obj)