diff --git a/.github/workflows/deploy_external.yaml b/.github/workflows/deploy_external.yaml index 1702f0c27c..cd8bca6661 100644 --- a/.github/workflows/deploy_external.yaml +++ b/.github/workflows/deploy_external.yaml @@ -5,7 +5,7 @@ on: workflows: - 'product_builder' branches: - - 'develop' + - 'feat/release-candidate' types: - completed permissions: diff --git a/.github/workflows/product_builder.yaml b/.github/workflows/product_builder.yaml index 0046302227..4c85893c19 100644 --- a/.github/workflows/product_builder.yaml +++ b/.github/workflows/product_builder.yaml @@ -69,9 +69,9 @@ jobs: crux: ${{ steps.filter.outputs.crux }} cruxui: ${{ steps.filter.outputs.cruxui }} kratos: ${{ steps.filter.outputs.kratos }} - tag: ${{ steps.settag.outputs.tag }} + tag: "0.15.0-rc" # ${{ steps.settag.outputs.tag }} extratag: ${{ steps.settag.outputs.extratag }} - version: ${{ steps.settag.outputs.version }} + version: "0.15.0-rc" # ${{ steps.settag.outputs.version }} minorversion: ${{ steps.settag.outputs.minorversion }} release: ${{ steps.release.outputs.release }} steps: @@ -669,11 +669,10 @@ jobs: defaults: run: working-directory: ${{ env.GOLANG_WORKING_DIRECTORY }} - needs: [gather_changes, e2e] + needs: [gather_changes, go_build] if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && - needs.e2e.result == 'success' && needs.go_build.result == 'success' && (needs.crux_build.result == 'success' || needs.crux_build.result == 'skipped') && (needs.crux-ui_build.result == 'success' || needs.crux-ui_build.result == 'skipped') && @@ -738,7 +737,6 @@ jobs: if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && - needs.e2e.result == 'success' && needs.go_build.result == 'success' && (needs.crux_build.result == 'success' || needs.crux_build.result == 'skipped') && (needs.crux-ui_build.result == 'success' || needs.crux-ui_build.result == 'skipped') && @@ -800,11 +798,10 @@ jobs: runs-on: ubuntu-22.04 container: image: ghcr.io/dyrector-io/dyrectorio/builder-images/signer:2 - needs: [crux_build, e2e, gather_changes] + needs: [crux_build, gather_changes] if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && - needs.e2e.result == 'success' && (needs.go_build.result == 'success' || needs.go_build.result == 'skipped') && needs.crux_build.result == 'success' && (needs.crux-ui_build.result == 'success' || needs.crux-ui_build.result == 'skipped') && @@ -863,11 +860,10 @@ jobs: runs-on: ubuntu-22.04 container: image: ghcr.io/dyrector-io/dyrectorio/builder-images/signer:2 - needs: [crux-ui_build, e2e, gather_changes] + needs: [crux-ui_build, gather_changes] if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && - needs.e2e.result == 'success' && (needs.go_build.result == 'success' || needs.go_build.result == 'skipped') && (needs.crux_build.result == 'success' || needs.crux_build.result == 'skipped') && needs.crux-ui_build.result == 'success' && @@ -926,11 +922,10 @@ jobs: runs-on: ubuntu-22.04 container: image: ghcr.io/dyrector-io/dyrectorio/builder-images/signer:2 - needs: [kratos_build, e2e, gather_changes] + needs: [kratos_build, gather_changes] if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && - needs.e2e.result == 'success' && (needs.go_build.result == 'success' || needs.go_build.result == 'skipped') && (needs.crux_build.result == 'success' || needs.crux_build.result == 'skipped') && (needs.crux-ui_build.result == 'success' || needs.crux-ui_build.result == 'skipped') && diff --git a/golang/internal/grpc/grpc.go b/golang/internal/grpc/grpc.go index 89ff2dd33c..1f4bc25753 100644 --- a/golang/internal/grpc/grpc.go +++ b/golang/internal/grpc/grpc.go @@ -78,6 +78,7 @@ type ClientLoop struct { type ( DeployFunc func(context.Context, *dogger.DeploymentLogger, *v1.DeployImageRequest, *v1.VersionData) error + DeploySharedSecretsFunc func(context.Context, string, map[string]string) error WatchContainerStatusFunc func(context.Context, string, bool) (*ContainerStatusStream, error) DeleteFunc func(context.Context, string, string) error SecretListFunc func(context.Context, string, string) ([]string, error) @@ -94,6 +95,7 @@ type ( type WorkerFunctions struct { Deploy DeployFunc + DeploySharedSecrets DeploySharedSecretsFunc WatchContainerStatus WatchContainerStatusFunc Delete DeleteFunc SecretList SecretListFunc @@ -186,7 +188,13 @@ func fetchCertificatesFromURL(ctx context.Context, addr string) (*x509.CertPool, func (cl *ClientLoop) grpcProcessCommand(command *agent.AgentCommand) { switch { case command.GetDeploy() != nil: - go executeVersionDeployRequest(cl.Ctx, command.GetDeploy(), cl.WorkerFuncs.Deploy, cl.AppConfig) + go executeDeployRequest( + cl.Ctx, + command.GetDeploy(), + cl.WorkerFuncs.Deploy, + cl.WorkerFuncs.DeploySharedSecrets, + cl.AppConfig, + ) case command.GetContainerState() != nil: go executeWatchContainerStatus(cl.Ctx, command.GetContainerState(), cl.WorkerFuncs.WatchContainerStatus) case command.GetContainerDelete() != nil: @@ -455,9 +463,9 @@ func (cl *ClientLoop) handleGrpcTokenError(err error, token *config.ValidJWT) { } } -func executeVersionDeployRequest( - ctx context.Context, req *agent.VersionDeployRequest, - deploy DeployFunc, appConfig *config.CommonConfiguration, +func executeDeployRequest( + ctx context.Context, req *agent.DeployRequest, + deploy DeployFunc, deploySecrets DeploySharedSecretsFunc, appConfig *config.CommonConfiguration, ) { if deploy == nil { log.Error().Msg("Deploy function not implemented") @@ -486,10 +494,27 @@ func executeVersionDeployRequest( return } - failed := false - var deployStatus common.DeploymentStatus + deployStatus := common.DeploymentStatus_FAILED + defer func() { + dog.WriteDeploymentStatus(deployStatus) + + err = statusStream.CloseSend() + if err != nil { + log.Error().Stack().Err(err).Str("deployment", req.Id).Msg("Status close error") + } + }() + + if len(req.Secrets) > 0 { + dog.WriteInfo("Deploying secrets") + err = deploySecrets(ctx, req.Prefix, req.Secrets) + if err != nil { + dog.WriteError(err.Error()) + return + } + } + for i := range req.Requests { - imageReq := mapper.MapDeployImage(req.Requests[i], appConfig) + imageReq := mapper.MapDeployImage(req.Prefix, req.Requests[i], appConfig) dog.SetRequestID(imageReq.RequestID) var versionData *v1.VersionData @@ -498,24 +523,12 @@ func executeVersionDeployRequest( } if err = deploy(ctx, dog, imageReq, versionData); err != nil { - failed = true dog.WriteError(err.Error()) + return } } - if failed { - deployStatus = common.DeploymentStatus_FAILED - } else { - deployStatus = common.DeploymentStatus_SUCCESSFUL - } - - dog.WriteDeploymentStatus(deployStatus) - - err = statusStream.CloseSend() - if err != nil { - log.Error().Stack().Err(err).Str("deployment", req.Id).Msg("Status close error") - return - } + deployStatus = common.DeploymentStatus_SUCCESSFUL } func streamContainerStatus( @@ -636,13 +649,10 @@ func executeDeleteMultipleContainers( req *common.DeleteContainersRequest, deleteFn DeleteContainersFunc, ) *AgentGrpcError { - var prefix, name string - if req.GetContainer() != nil { - prefix = req.GetContainer().Prefix - name = req.GetContainer().Name - } else { - prefix = req.GetPrefix() - name = "" + prefix, name, err := mapper.MapContainerOrPrefixToPrefixName(req.Target) + if err != nil { + log.Error().Err(err).Msg("Failed to delete multiple containers") + return agentError(ctx, err) } ctx = metadata.AppendToOutgoingContext(ctx, "dyo-container-prefix", prefix, "dyo-container-name", name) @@ -653,7 +663,7 @@ func executeDeleteMultipleContainers( log.Info().Msg("Deleting multiple containers") - err := deleteFn(ctx, req) + err = deleteFn(ctx, req) if err != nil { log.Error().Stack().Err(err).Msg("Failed to delete multiple containers") return agentError(ctx, err) @@ -741,8 +751,15 @@ func executeSecretList( listFunc SecretListFunc, appConfig *config.CommonConfiguration, ) *AgentGrpcError { - prefix := command.Container.Prefix - name := command.Container.Name + var prefix string + name := "" + + if command.Target.GetContainer() != nil { + prefix = command.Target.GetContainer().Prefix + name = command.Target.GetContainer().Name + } else { + prefix = command.Target.GetPrefix() + } ctx = metadata.AppendToOutgoingContext(ctx, "dyo-container-prefix", prefix, "dyo-container-name", name) @@ -765,10 +782,8 @@ func executeSecretList( } resp := &common.ListSecretsResponse{ - Prefix: prefix, - Name: name, + Target: command.Target, PublicKey: publicKey, - HasKeys: keys != nil, Keys: keys, } diff --git a/golang/internal/mapper/grpc.go b/golang/internal/mapper/grpc.go index c30b2aeb77..c87a791a10 100644 --- a/golang/internal/mapper/grpc.go +++ b/golang/internal/mapper/grpc.go @@ -1,6 +1,7 @@ package mapper import ( + "errors" "fmt" "strings" "time" @@ -25,33 +26,18 @@ import ( corev1 "k8s.io/api/core/v1" ) -func mapInstanceConfig(in *agent.InstanceConfig) v1.InstanceConfig { - instanceConfig := v1.InstanceConfig{ - ContainerPreName: in.Prefix, - Name: in.Prefix, - SharedEnvironment: map[string]string{}, - } - - if in.RepositoryPrefix != nil { - instanceConfig.RepositoryPreName = *in.RepositoryPrefix - } - - if in.MountPath != nil { - instanceConfig.MountPath = *in.MountPath - } +var ErrNoTargetContainerOrPrefix = errors.New("no target container or prefix") - if in.Environment != nil { - instanceConfig.Environment = in.Environment - } - - return instanceConfig -} - -func MapDeployImage(req *agent.DeployRequest, appConfig *config.CommonConfiguration) *v1.DeployImageRequest { +func MapDeployImage(prefix string, req *agent.DeployWorkloadRequest, appConfig *config.CommonConfiguration) *v1.DeployImageRequest { res := &v1.DeployImageRequest{ - RequestID: req.Id, - InstanceConfig: mapInstanceConfig(req.InstanceConfig), - ContainerConfig: mapContainerConfig(req), + RequestID: req.Id, + InstanceConfig: v1.InstanceConfig{ + UseSharedEnvs: false, + Environment: map[string]string{}, + SharedEnvironment: map[string]string{}, + ContainerPreName: prefix, + }, + ContainerConfig: mapContainerConfig(prefix, req), ImageName: req.ImageName, Tag: req.Tag, Registry: req.Registry, @@ -68,22 +54,18 @@ func MapDeployImage(req *agent.DeployRequest, appConfig *config.CommonConfigurat v1.SetDeploymentDefaults(res, appConfig) - if req.RuntimeConfig != nil { - res.RuntimeConfig = v1.Base64JSONBytes(*req.RuntimeConfig) - } - if req.Registry != nil { res.Registry = req.Registry } return res } -func mapContainerConfig(in *agent.DeployRequest) v1.ContainerConfig { +func mapContainerConfig(prefix string, in *agent.DeployWorkloadRequest) v1.ContainerConfig { cc := in.Common containerConfig := v1.ContainerConfig{ Container: cc.Name, - ContainerPreName: in.InstanceConfig.Prefix, + ContainerPreName: prefix, Ports: MapPorts(cc.Ports), PortRanges: mapPortRanges(cc.PortRanges), Volumes: mapVolumes(cc.Volumes), @@ -692,3 +674,19 @@ func MapDockerContainerEventToContainerState(event string) common.ContainerState return common.ContainerState_CONTAINER_STATE_UNSPECIFIED } } + +func MapContainerOrPrefixToPrefixName(target *common.ContainerOrPrefix) (prefix, name string, err error) { + if target == nil { + return "", "", ErrNoTargetContainerOrPrefix + } + + if target.GetContainer() != nil { + return target.GetContainer().GetPrefix(), target.GetContainer().Name, nil + } + + if target.GetPrefix() == "" { + return "", "", ErrNoTargetContainerOrPrefix + } + + return target.GetPrefix(), "", nil +} diff --git a/golang/internal/mapper/grpc_test.go b/golang/internal/mapper/grpc_test.go index b4b28a4ae0..08e8e57ae7 100644 --- a/golang/internal/mapper/grpc_test.go +++ b/golang/internal/mapper/grpc_test.go @@ -24,7 +24,7 @@ func TestMapDeployImageRequest(t *testing.T) { req := testDeployRequest() cfg := testAppConfig() - res := MapDeployImage(req, cfg) + res := MapDeployImage("", req, cfg) expected := testExpectedCommon(req) assert.Equal(t, expected, res) @@ -52,12 +52,12 @@ func TestMapDeployImageRequestRestartPolicies(t *testing.T) { for _, tC := range cases { req.Dagent.RestartPolicy = tC.policy expected.ContainerConfig.RestartPolicy = tC.dockerType - res := MapDeployImage(req, cfg) + res := MapDeployImage("", req, cfg) assert.Equal(t, expected, res) } } -func testExpectedCommon(req *agent.DeployRequest) *v1.DeployImageRequest { +func testExpectedCommon(req *agent.DeployWorkloadRequest) *v1.DeployImageRequest { return &v1.DeployImageRequest{ RequestID: "testID", RegistryAuth: &image.RegistryAuth{ @@ -67,17 +67,13 @@ func testExpectedCommon(req *agent.DeployRequest) *v1.DeployImageRequest { Password: "test-pass", }, InstanceConfig: v1.InstanceConfig{ - ContainerPreName: "test-prefix", - MountPath: "/path/to/mount", - Name: "test-prefix", - Environment: map[string]string{"Evn1": "Val1", "Env2": "Val2"}, - Registry: "", - RepositoryPreName: "repo-prefix", - SharedEnvironment: map[string]string{}, UseSharedEnvs: false, + Environment: map[string]string{}, + SharedEnvironment: map[string]string{}, + ContainerPreName: "", }, ContainerConfig: v1.ContainerConfig{ - ContainerPreName: "test-prefix", + ContainerPreName: "", Container: "test-common-config", Ports: []builder.PortBinding{{ExposedPort: 0x4d2, PortBinding: pointer.ToUint16(0x1a85)}}, PortRanges: []builder.PortRangeBinding{{Internal: builder.PortRange{From: 0x0, To: 0x18}, External: builder.PortRange{From: 0x40, To: 0x80}}}, @@ -153,7 +149,7 @@ func testExpectedCommon(req *agent.DeployRequest) *v1.DeployImageRequest { UseLoadBalancer: true, ExtraLBAnnotations: map[string]string{"annotation1": "value1"}, }, - RuntimeConfig: v1.Base64JSONBytes{0x6b, 0x65, 0x79, 0x31, 0x3d, 0x76, 0x61, 0x6c, 0x31, 0x2c, 0x6b, 0x65, 0x79, 0x32, 0x3d, 0x76, 0x61, 0x6c, 0x32}, // encoded string: a2V5MT12YWwxLGtleTI9dmFsMg== + RuntimeConfig: nil, Registry: req.Registry, ImageName: "test-image", Tag: "test-tag", @@ -210,24 +206,19 @@ func TestMapDockerContainerEventToContainerState(t *testing.T) { assert.Equal(t, common.ContainerState_EXITED, MapDockerContainerEventToContainerState("die")) } -func testDeployRequest() *agent.DeployRequest { +func testDeployRequest() *agent.DeployWorkloadRequest { registry := "https://my-registry.com" - runtimeCfg := "key1=val1,key2=val2" var uid int64 = 777 upLimit := "5Mi" - mntPath := "/path/to/mount" - repoPrefix := "repo-prefix" strategy := common.ExposeStrategy_EXPOSE_WITH_TLS b := true - return &agent.DeployRequest{ - Id: "testID", - ContainerName: "test-container", - ImageName: "test-image", - Tag: "test-tag", - Registry: ®istry, - RuntimeConfig: &runtimeCfg, - Dagent: testDagentConfig(), - Crane: testCraneConfig(), + return &agent.DeployWorkloadRequest{ + Id: "testID", + ImageName: "test-image", + Tag: "test-tag", + Registry: ®istry, + Dagent: testDagentConfig(), + Crane: testCraneConfig(), Common: &agent.CommonContainerConfig{ Name: "test-common-config", Commands: []string{"make", "test"}, @@ -274,12 +265,6 @@ func testDeployRequest() *agent.DeployRequest { Password: "test-pass", Url: "https://test-url.com", }, - InstanceConfig: &agent.InstanceConfig{ - Prefix: "test-prefix", - MountPath: &mntPath, - RepositoryPrefix: &repoPrefix, - Environment: map[string]string{"Evn1": "Val1", "Env2": "Val2"}, - }, } } @@ -411,7 +396,7 @@ func testAppConfig() *config.CommonConfiguration { } } -func testDeployRequestWithLogDriver(driverType common.DriverType) *agent.DeployRequest { +func testDeployRequestWithLogDriver(driverType common.DriverType) *agent.DeployWorkloadRequest { request := testDeployRequest() if driverType == common.DriverType_NODE_DEFAULT { request.Dagent.LogConfig = nil @@ -421,7 +406,7 @@ func testDeployRequestWithLogDriver(driverType common.DriverType) *agent.DeployR return request } -func testExpectedCommonWithLogConfigType(req *agent.DeployRequest, logConfigType string) *v1.DeployImageRequest { +func testExpectedCommonWithLogConfigType(req *agent.DeployWorkloadRequest, logConfigType string) *v1.DeployImageRequest { expected := testExpectedCommon(req) if req.Dagent.LogConfig == nil { expected.ContainerConfig.LogConfig = nil @@ -508,7 +493,7 @@ func TestMapDeployImageLogConfig(t *testing.T) { req := testDeployRequestWithLogDriver(tC.driver) cfg := testAppConfig() - res := MapDeployImage(req, cfg) + res := MapDeployImage("", req, cfg) expected := testExpectedCommonWithLogConfigType(req, tC.want) assert.Equal(t, expected, res) diff --git a/golang/pkg/crane/crane.go b/golang/pkg/crane/crane.go index 3343fd7223..6cc77ca2b5 100644 --- a/golang/pkg/crane/crane.go +++ b/golang/pkg/crane/crane.go @@ -39,6 +39,7 @@ func Serve(cfg *config.Configuration, secretStore commonConfig.SecretStore) { grpcContext := grpc.WithGRPCConfig(context.Background(), cfg) grpc.Init(grpcContext, &cfg.CommonConfiguration, secretStore, &grpc.WorkerFunctions{ Deploy: k8s.Deploy, + DeploySharedSecrets: k8s.DeploySharedSecrets, WatchContainerStatus: crux.WatchDeploymentsByPrefix, Delete: k8s.Delete, ContainerCommand: crux.DeploymentCommand, diff --git a/golang/pkg/crane/crux/deploy.go b/golang/pkg/crane/crux/deploy.go index ec67fba3f3..e61703eb13 100644 --- a/golang/pkg/crane/crux/deploy.go +++ b/golang/pkg/crane/crux/deploy.go @@ -54,6 +54,10 @@ func GetSecretsList(ctx context.Context, prefix, name string) ([]string, error) cfg := grpc.GetConfigFromContext(ctx).(*config.Configuration) secretHandler := k8s.NewSecret(ctx, k8s.NewClient(cfg)) + if name == "" { + name = prefix + "-shared" + } + return secretHandler.ListSecrets(prefix, name) } diff --git a/golang/pkg/crane/k8s/delete_facade.go b/golang/pkg/crane/k8s/delete_facade.go index 97e4e64953..dec20fb386 100644 --- a/golang/pkg/crane/k8s/delete_facade.go +++ b/golang/pkg/crane/k8s/delete_facade.go @@ -2,11 +2,11 @@ package k8s import ( "context" - "fmt" "github.com/rs/zerolog/log" "github.com/dyrector-io/dyrectorio/golang/internal/grpc" + "github.com/dyrector-io/dyrectorio/golang/internal/mapper" "github.com/dyrector-io/dyrectorio/golang/pkg/crane/config" "github.com/dyrector-io/dyrectorio/protobuf/go/common" @@ -64,14 +64,18 @@ func (d *DeleteFacade) DeleteIngresses() error { // hard-delete if called with prefix name only without container name func DeleteMultiple(c context.Context, request *common.DeleteContainersRequest) error { cfg := grpc.GetConfigFromContext(c).(*config.Configuration) - if ns := request.GetContainer().GetPrefix(); ns != "" { - if deploymentName := request.GetContainer().GetName(); deploymentName != "" { - return Delete(c, ns, deploymentName) - } - del := NewDeleteFacade(c, ns, "", cfg) - return del.DeleteNamespace(ns) + + prefix, name, err := mapper.MapContainerOrPrefixToPrefixName(request.Target) + if err != nil { + return err } - return fmt.Errorf("invalid DeleteContainers request") + + if name != "" { + return Delete(c, prefix, name) + } + + del := NewDeleteFacade(c, prefix, "", cfg) + return del.DeleteNamespace(prefix) } // soft-delete: deployment,services,configmaps, ingresses diff --git a/golang/pkg/crane/k8s/deploy_facade.go b/golang/pkg/crane/k8s/deploy_facade.go index 5a922417d1..31d20edf40 100644 --- a/golang/pkg/crane/k8s/deploy_facade.go +++ b/golang/pkg/crane/k8s/deploy_facade.go @@ -262,6 +262,20 @@ func (d *DeployFacade) Clear() error { return nil } +func DeploySharedSecrets(c context.Context, prefix string, secrets map[string]string) error { + cfg := grpc.GetConfigFromContext(c).(*config.Configuration) + + k8sClient := NewClient(cfg) + secret := NewSecret(c, k8sClient) + + err := secret.applySecrets(prefix, prefix+"-shared", secrets) + if err != nil { + return fmt.Errorf("could not write secrets, aborting: %w", err) + } + + return nil +} + func Deploy(c context.Context, dog *dogger.DeploymentLogger, deployImageRequest *v1.DeployImageRequest, _ *v1.VersionData, ) error { diff --git a/golang/pkg/dagent/dagent.go b/golang/pkg/dagent/dagent.go index 5205deec31..09d7901ccd 100644 --- a/golang/pkg/dagent/dagent.go +++ b/golang/pkg/dagent/dagent.go @@ -36,6 +36,7 @@ func Serve(cfg *config.Configuration) { grpcContext := grpc.WithGRPCConfig(context.Background(), cfg) grpc.Init(grpcContext, &cfg.CommonConfiguration, cfg, &grpc.WorkerFunctions{ Deploy: utils.DeployImage, + DeploySharedSecrets: utils.DeploySharedSecrets, WatchContainerStatus: utils.ContainerStateStream, Delete: utils.DeleteContainerByPrefixAndName, SecretList: utils.SecretList, diff --git a/golang/pkg/dagent/utils/docker.go b/golang/pkg/dagent/utils/docker.go index 23eb552cd5..a52e1a89d8 100644 --- a/golang/pkg/dagent/utils/docker.go +++ b/golang/pkg/dagent/utils/docker.go @@ -317,6 +317,21 @@ func waitForContainer( return <-errorChannel } +func DeploySharedSecrets(ctx context.Context, + prefix string, + secrets map[string]string, +) error { + cfg := grpc.GetConfigFromContext(ctx).(*config.Configuration) + + pf := NewSecretsPrefixFile(cfg.InternalMountPath, prefix) + err := pf.WriteVariables(secrets) + if err != nil { + return fmt.Errorf("could not write secrets, aborting: %w", err) + } + + return nil +} + //nolint:funlen,gocyclo // TODO(@nandor-magyar): refactor this function into smaller parts func DeployImage(ctx context.Context, dog *dogger.DeploymentLogger, @@ -348,11 +363,9 @@ func DeployImage(ctx context.Context, log.Debug().Str("name", deployImageRequest.ImageName).Str("full", expandedImageName).Msg("Image name parsed") logDeployInfo(dog, deployImageRequest, expandedImageName, containerName) + pf := NewSharedEnvPrefixFile(cfg.InternalMountPath, prefix) if len(deployImageRequest.InstanceConfig.SharedEnvironment) > 0 { - err = WriteSharedEnvironmentVariables( - cfg.InternalMountPath, - prefix, - deployImageRequest.InstanceConfig.SharedEnvironment) + err = pf.WriteVariables(deployImageRequest.InstanceConfig.SharedEnvironment) if err != nil { dog.WriteError("could not write shared environment variables, aborting...", err.Error()) return err @@ -362,8 +375,7 @@ func DeployImage(ctx context.Context, var envMap map[string]string if deployImageRequest.InstanceConfig.UseSharedEnvs { - envMap, err = ReadSharedEnvironmentVariables(cfg.InternalMountPath, - prefix) + envMap, err = pf.ReadVariables() if err != nil { dog.WriteError("could not load shared environment variables, while useSharedEnvs is on, aborting...", err.Error()) return err @@ -540,6 +552,8 @@ func getContainerName(deployImageRequest *v1.DeployImageRequest) string { func getContainerPrefix(deployImageRequest *v1.DeployImageRequest) string { containerPrefix := "" + // TODO (@nandor-magyar): the line below is probably wrong, fix it if you dare + // soon to be removed though, as merging is done by crux if deployImageRequest.ContainerConfig.Container != "" { if deployImageRequest.InstanceConfig.MountPath != "" { containerPrefix = deployImageRequest.InstanceConfig.MountPath @@ -547,6 +561,11 @@ func getContainerPrefix(deployImageRequest *v1.DeployImageRequest) string { containerPrefix = deployImageRequest.InstanceConfig.ContainerPreName } } + + if containerPrefix == "" { + containerPrefix = deployImageRequest.ContainerConfig.ContainerPreName + } + return containerPrefix } @@ -710,6 +729,18 @@ func setImageLabels(expandedImageName string, } func SecretList(ctx context.Context, prefix, name string) ([]string, error) { + if name == "" { + cfg := grpc.GetConfigFromContext(ctx).(*config.Configuration) + + pf := NewSecretsPrefixFile(cfg.InternalMountPath, prefix) + secrets, err := pf.ReadVariables() + if err != nil { + return []string{}, fmt.Errorf("could not read secrets, aborting: %w", err) + } + + return maps.Keys(secrets), nil + } + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return nil, err @@ -717,7 +748,7 @@ func SecretList(ctx context.Context, prefix, name string) ([]string, error) { containers, err := cli.ContainerList(ctx, container.ListOptions{ All: true, - Filters: filters.NewArgs(filters.KeyValuePair{Key: "name", Value: fmt.Sprintf("^/?%s-%s$", prefix, name)}), + Filters: filters.NewArgs(filters.KeyValuePair{Key: "name", Value: fmt.Sprintf("^/?%s$", util.JoinV("-", prefix, name))}), }) if err != nil { return nil, err @@ -771,13 +802,15 @@ func ContainerCommand(ctx context.Context, command *common.ContainerCommandReque } func DeleteContainers(ctx context.Context, request *common.DeleteContainersRequest) error { - var err error - if request.GetContainer() != nil { - err = DeleteContainerByPrefixAndName(ctx, request.GetContainer().Prefix, request.GetContainer().Name) - } else if request.GetPrefix() != "" { - err = dockerHelper.DeleteContainersByLabel(ctx, label.GetPrefixLabelFilter(request.GetPrefix())) + prefix, name, err := mapper.MapContainerOrPrefixToPrefixName(request.Target) + if err != nil { + return err + } + + if name != "" { + err = DeleteContainerByPrefixAndName(ctx, prefix, name) } else { - log.Error().Msg("Unknown DeleteContainers request") + err = dockerHelper.DeleteContainersByLabel(ctx, label.GetPrefixLabelFilter(prefix)) } return err diff --git a/golang/pkg/dagent/utils/environment.go b/golang/pkg/dagent/utils/environment.go deleted file mode 100644 index 02d97a4f35..0000000000 --- a/golang/pkg/dagent/utils/environment.go +++ /dev/null @@ -1,85 +0,0 @@ -package utils - -import ( - "fmt" - "io/fs" - "os" - "path/filepath" - - "github.com/joho/godotenv" -) - -const dirPerm = 0o700 - -type SharedVariableParamError struct { - variable string -} - -func (e SharedVariableParamError) Error() string { - return fmt.Sprintf("variable %s was empty and it is necessary for shared environment variables", e.variable) -} - -func NewErrSharedVariableParamEmpty(param string) SharedVariableParamError { - return SharedVariableParamError{variable: param} -} - -func WriteSharedEnvironmentVariables(dataRoot, prefix string, in map[string]string) error { - var err error - err = validatePath(dataRoot, prefix) - if err != nil { - return err - } - - out, err := godotenv.Marshal(in) - if err != nil { - return err - } - sharedEnvDirPath := getSharedEnvDir(dataRoot, prefix) - sharedEnvFilePath := getSharedEnvPath(dataRoot, prefix) - - err = os.MkdirAll(sharedEnvDirPath, dirPerm) - if err != nil { - return err - } - - err = os.WriteFile(sharedEnvFilePath, []byte(out), fs.ModePerm) - if err != nil { - return err - } - - return nil -} - -func validatePath(dataRoot, prefix string) error { - if dataRoot == "" { - return NewErrSharedVariableParamEmpty("dataRoot") - } else if prefix == "" { - return NewErrSharedVariableParamEmpty("prefix") - } - - return nil -} - -func getSharedEnvDir(dataRoot, prefix string) string { - return filepath.Join(dataRoot, prefix) -} - -func getSharedEnvPath(dataRoot, prefix string) string { - return filepath.Join(getSharedEnvDir(dataRoot, prefix), ".shared-env") -} - -func ReadSharedEnvironmentVariables(dataRoot, prefix string) (map[string]string, error) { - var err error - err = validatePath(dataRoot, prefix) - if err != nil { - return nil, err - } - sharedEnvPath := getSharedEnvPath(dataRoot, prefix) - - sharedEnvsFile, err := os.ReadFile(sharedEnvPath) // #nosec G304 -- shared-envs are generated from prefix+name and those are RFC1039 - if err != nil { - return nil, err - } - - return godotenv.UnmarshalBytes(sharedEnvsFile) -} diff --git a/golang/pkg/dagent/utils/prefix_file.go b/golang/pkg/dagent/utils/prefix_file.go new file mode 100644 index 0000000000..09754b668e --- /dev/null +++ b/golang/pkg/dagent/utils/prefix_file.go @@ -0,0 +1,112 @@ +package utils + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/joho/godotenv" +) + +const dirPerm = 0o700 + +type PrefixFileParamError struct { + variable string +} + +func (e PrefixFileParamError) Error() string { + return fmt.Sprintf("variable %s was empty and it is necessary for shared environment variables", e.variable) +} + +func NewErrPrefixFileParamEmpty(param string) PrefixFileParamError { + return PrefixFileParamError{variable: param} +} + +type PrefixFile struct { + DataRoot string + Prefix string + FileName string +} + +func NewSharedEnvPrefixFile(dataRoot, prefix string) PrefixFile { + return PrefixFile{ + DataRoot: dataRoot, + Prefix: prefix, + FileName: ".shared-env", + } +} + +func NewSecretsPrefixFile(dataRoot, prefix string) PrefixFile { + return PrefixFile{ + DataRoot: dataRoot, + Prefix: prefix, + FileName: ".shared-secrets", + } +} + +func (pf *PrefixFile) validatePath() error { + if pf.DataRoot == "" { + return NewErrPrefixFileParamEmpty("dataRoot") + } else if pf.Prefix == "" { + return NewErrPrefixFileParamEmpty("prefix") + } + + return nil +} + +func (pf *PrefixFile) getDirectory() string { + return filepath.Join(pf.DataRoot, pf.Prefix) +} + +func (pf *PrefixFile) getFilePath() string { + return filepath.Join(pf.getDirectory(), pf.FileName) +} + +func (pf *PrefixFile) ReadVariables() (map[string]string, error) { + err := pf.validatePath() + if err != nil { + return nil, err + } + + filePath := pf.getFilePath() + + file, err := os.ReadFile(filePath) // #nosec G304 -- shared-envs are generated from prefix+name and those are RFC1039 + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return map[string]string{}, nil + } + + return nil, err + } + + return godotenv.UnmarshalBytes(file) +} + +func (pf *PrefixFile) WriteVariables(in map[string]string) error { + var err error + err = pf.validatePath() + if err != nil { + return err + } + + out, err := godotenv.Marshal(in) + if err != nil { + return err + } + dirPath := pf.getDirectory() + filePath := pf.getFilePath() + + err = os.MkdirAll(dirPath, dirPerm) + if err != nil { + return err + } + + err = os.WriteFile(filePath, []byte(out), fs.ModePerm) + if err != nil { + return err + } + + return nil +} diff --git a/golang/pkg/dagent/utils/environment_test.go b/golang/pkg/dagent/utils/prefix_file_test.go similarity index 85% rename from golang/pkg/dagent/utils/environment_test.go rename to golang/pkg/dagent/utils/prefix_file_test.go index a4d12bbbcb..8d656ed38f 100644 --- a/golang/pkg/dagent/utils/environment_test.go +++ b/golang/pkg/dagent/utils/prefix_file_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestWriteSharedEnvironmentVariables(t *testing.T) { +func TestWriteVariables(t *testing.T) { tests := []struct { name string dataRoot string @@ -37,13 +37,15 @@ func TestWriteSharedEnvironmentVariables(t *testing.T) { prefix: "prefix", instanceName: "instance", inputVariables: map[string]string{"key1": "value1", "key2": "value2"}, - expectedError: utils.NewErrSharedVariableParamEmpty("dataRoot"), + expectedError: utils.NewErrPrefixFileParamEmpty("dataRoot"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := utils.WriteSharedEnvironmentVariables(tt.dataRoot, tt.prefix, tt.inputVariables) + pf := utils.NewSharedEnvPrefixFile(tt.dataRoot, tt.prefix) + + err := pf.WriteVariables(tt.inputVariables) assert.Equal(t, tt.expectedError, err) if err == nil { @@ -60,7 +62,7 @@ func TestWriteSharedEnvironmentVariables(t *testing.T) { } } -func TestReadSharedEnvironmentVariables(t *testing.T) { +func TestReadVariables(t *testing.T) { tests := []struct { name string dataRoot string @@ -81,6 +83,8 @@ func TestReadSharedEnvironmentVariables(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + pf := utils.NewSharedEnvPrefixFile(tt.dataRoot, tt.prefix) + sharedEnvPath := filepath.Join(tt.dataRoot, tt.prefix, ".shared-env") err := os.WriteFile(sharedEnvPath, []byte(tt.fileContent), fs.ModePerm) assert.NoError(t, err) @@ -90,7 +94,7 @@ func TestReadSharedEnvironmentVariables(t *testing.T) { assert.NoError(t, removeErr) }() - result, err := utils.ReadSharedEnvironmentVariables(tt.dataRoot, tt.prefix) + result, err := pf.ReadVariables() assert.Equal(t, tt.expectedError, err) assert.Equal(t, tt.expectedResult, result) }) diff --git a/protobuf/go/agent/agent.pb.go b/protobuf/go/agent/agent.pb.go index fb3337b1fc..30837f2dbb 100644 --- a/protobuf/go/agent/agent.pb.go +++ b/protobuf/go/agent/agent.pb.go @@ -215,7 +215,7 @@ func (m *AgentCommand) GetCommand() isAgentCommand_Command { return nil } -func (x *AgentCommand) GetDeploy() *VersionDeployRequest { +func (x *AgentCommand) GetDeploy() *DeployRequest { if x, ok := x.GetCommand().(*AgentCommand_Deploy); ok { return x.Deploy } @@ -304,7 +304,7 @@ type isAgentCommand_Command interface { } type AgentCommand_Deploy struct { - Deploy *VersionDeployRequest `protobuf:"bytes,1,opt,name=deploy,proto3,oneof"` + Deploy *DeployRequest `protobuf:"bytes,1,opt,name=deploy,proto3,oneof"` } type AgentCommand_ContainerState struct { @@ -588,19 +588,21 @@ func (x *DeployResponse) GetStarted() bool { return false } -type VersionDeployRequest struct { +type DeployRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - VersionName string `protobuf:"bytes,2,opt,name=versionName,proto3" json:"versionName,omitempty"` - ReleaseNotes string `protobuf:"bytes,3,opt,name=releaseNotes,proto3" json:"releaseNotes,omitempty"` - Requests []*DeployRequest `protobuf:"bytes,4,rep,name=requests,proto3" json:"requests,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + VersionName string `protobuf:"bytes,2,opt,name=versionName,proto3" json:"versionName,omitempty"` + ReleaseNotes string `protobuf:"bytes,3,opt,name=releaseNotes,proto3" json:"releaseNotes,omitempty"` + Prefix string `protobuf:"bytes,4,opt,name=prefix,proto3" json:"prefix,omitempty"` + Secrets map[string]string `protobuf:"bytes,5,rep,name=secrets,proto3" json:"secrets,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Requests []*DeployWorkloadRequest `protobuf:"bytes,6,rep,name=requests,proto3" json:"requests,omitempty"` } -func (x *VersionDeployRequest) Reset() { - *x = VersionDeployRequest{} +func (x *DeployRequest) Reset() { + *x = DeployRequest{} if protoimpl.UnsafeEnabled { mi := &file_protobuf_proto_agent_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -608,13 +610,13 @@ func (x *VersionDeployRequest) Reset() { } } -func (x *VersionDeployRequest) String() string { +func (x *DeployRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*VersionDeployRequest) ProtoMessage() {} +func (*DeployRequest) ProtoMessage() {} -func (x *VersionDeployRequest) ProtoReflect() protoreflect.Message { +func (x *DeployRequest) ProtoReflect() protoreflect.Message { mi := &file_protobuf_proto_agent_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -626,33 +628,47 @@ func (x *VersionDeployRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use VersionDeployRequest.ProtoReflect.Descriptor instead. -func (*VersionDeployRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use DeployRequest.ProtoReflect.Descriptor instead. +func (*DeployRequest) Descriptor() ([]byte, []int) { return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{5} } -func (x *VersionDeployRequest) GetId() string { +func (x *DeployRequest) GetId() string { if x != nil { return x.Id } return "" } -func (x *VersionDeployRequest) GetVersionName() string { +func (x *DeployRequest) GetVersionName() string { if x != nil { return x.VersionName } return "" } -func (x *VersionDeployRequest) GetReleaseNotes() string { +func (x *DeployRequest) GetReleaseNotes() string { if x != nil { return x.ReleaseNotes } return "" } -func (x *VersionDeployRequest) GetRequests() []*DeployRequest { +func (x *DeployRequest) GetPrefix() string { + if x != nil { + return x.Prefix + } + return "" +} + +func (x *DeployRequest) GetSecrets() map[string]string { + if x != nil { + return x.Secrets + } + return nil +} + +func (x *DeployRequest) GetRequests() []*DeployWorkloadRequest { if x != nil { return x.Requests } @@ -665,7 +681,7 @@ type ListSecretsRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Container *common.ContainerIdentifier `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"` + Target *common.ContainerOrPrefix `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` } func (x *ListSecretsRequest) Reset() { @@ -700,88 +716,13 @@ func (*ListSecretsRequest) Descriptor() ([]byte, []int) { return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{6} } -func (x *ListSecretsRequest) GetContainer() *common.ContainerIdentifier { +func (x *ListSecretsRequest) GetTarget() *common.ContainerOrPrefix { if x != nil { - return x.Container + return x.Target } return nil } -// * -// Deploys a single container -type InstanceConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // prefix mapped into host folder structure, - // used as namespace id - Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` - MountPath *string `protobuf:"bytes,2,opt,name=mountPath,proto3,oneof" json:"mountPath,omitempty"` // mount path of instance (docker only) - Environment map[string]string `protobuf:"bytes,3,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // environment variable map - RepositoryPrefix *string `protobuf:"bytes,4,opt,name=repositoryPrefix,proto3,oneof" json:"repositoryPrefix,omitempty"` // registry repo prefix -} - -func (x *InstanceConfig) Reset() { - *x = InstanceConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *InstanceConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*InstanceConfig) ProtoMessage() {} - -func (x *InstanceConfig) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[7] - 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 InstanceConfig.ProtoReflect.Descriptor instead. -func (*InstanceConfig) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{7} -} - -func (x *InstanceConfig) GetPrefix() string { - if x != nil { - return x.Prefix - } - return "" -} - -func (x *InstanceConfig) GetMountPath() string { - if x != nil && x.MountPath != nil { - return *x.MountPath - } - return "" -} - -func (x *InstanceConfig) GetEnvironment() map[string]string { - if x != nil { - return x.Environment - } - return nil -} - -func (x *InstanceConfig) GetRepositoryPrefix() string { - if x != nil && x.RepositoryPrefix != nil { - return *x.RepositoryPrefix - } - return "" -} - type RegistryAuth struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -796,7 +737,7 @@ type RegistryAuth struct { func (x *RegistryAuth) Reset() { *x = RegistryAuth{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[8] + mi := &file_protobuf_proto_agent_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -809,7 +750,7 @@ func (x *RegistryAuth) String() string { func (*RegistryAuth) ProtoMessage() {} func (x *RegistryAuth) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[8] + mi := &file_protobuf_proto_agent_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -822,7 +763,7 @@ func (x *RegistryAuth) ProtoReflect() protoreflect.Message { // Deprecated: Use RegistryAuth.ProtoReflect.Descriptor instead. func (*RegistryAuth) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{8} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{7} } func (x *RegistryAuth) GetName() string { @@ -865,7 +806,7 @@ type Port struct { func (x *Port) Reset() { *x = Port{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[9] + mi := &file_protobuf_proto_agent_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -878,7 +819,7 @@ func (x *Port) String() string { func (*Port) ProtoMessage() {} func (x *Port) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[9] + mi := &file_protobuf_proto_agent_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -891,7 +832,7 @@ func (x *Port) ProtoReflect() protoreflect.Message { // Deprecated: Use Port.ProtoReflect.Descriptor instead. func (*Port) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{9} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{8} } func (x *Port) GetInternal() int32 { @@ -920,7 +861,7 @@ type PortRange struct { func (x *PortRange) Reset() { *x = PortRange{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[10] + mi := &file_protobuf_proto_agent_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -933,7 +874,7 @@ func (x *PortRange) String() string { func (*PortRange) ProtoMessage() {} func (x *PortRange) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[10] + mi := &file_protobuf_proto_agent_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -946,7 +887,7 @@ func (x *PortRange) ProtoReflect() protoreflect.Message { // Deprecated: Use PortRange.ProtoReflect.Descriptor instead. func (*PortRange) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{10} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{9} } func (x *PortRange) GetFrom() int32 { @@ -975,7 +916,7 @@ type PortRangeBinding struct { func (x *PortRangeBinding) Reset() { *x = PortRangeBinding{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[11] + mi := &file_protobuf_proto_agent_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -988,7 +929,7 @@ func (x *PortRangeBinding) String() string { func (*PortRangeBinding) ProtoMessage() {} func (x *PortRangeBinding) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[11] + mi := &file_protobuf_proto_agent_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1001,7 +942,7 @@ func (x *PortRangeBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use PortRangeBinding.ProtoReflect.Descriptor instead. func (*PortRangeBinding) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{11} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{10} } func (x *PortRangeBinding) GetInternal() *PortRange { @@ -1033,7 +974,7 @@ type Volume struct { func (x *Volume) Reset() { *x = Volume{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[12] + mi := &file_protobuf_proto_agent_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1046,7 +987,7 @@ func (x *Volume) String() string { func (*Volume) ProtoMessage() {} func (x *Volume) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[12] + mi := &file_protobuf_proto_agent_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1059,7 +1000,7 @@ func (x *Volume) ProtoReflect() protoreflect.Message { // Deprecated: Use Volume.ProtoReflect.Descriptor instead. func (*Volume) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{12} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{11} } func (x *Volume) GetName() string { @@ -1109,7 +1050,7 @@ type VolumeLink struct { func (x *VolumeLink) Reset() { *x = VolumeLink{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[13] + mi := &file_protobuf_proto_agent_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1122,7 +1063,7 @@ func (x *VolumeLink) String() string { func (*VolumeLink) ProtoMessage() {} func (x *VolumeLink) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[13] + mi := &file_protobuf_proto_agent_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1135,7 +1076,7 @@ func (x *VolumeLink) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeLink.ProtoReflect.Descriptor instead. func (*VolumeLink) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{13} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{12} } func (x *VolumeLink) GetName() string { @@ -1169,7 +1110,7 @@ type InitContainer struct { func (x *InitContainer) Reset() { *x = InitContainer{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[14] + mi := &file_protobuf_proto_agent_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1182,7 +1123,7 @@ func (x *InitContainer) String() string { func (*InitContainer) ProtoMessage() {} func (x *InitContainer) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[14] + mi := &file_protobuf_proto_agent_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1195,7 +1136,7 @@ func (x *InitContainer) ProtoReflect() protoreflect.Message { // Deprecated: Use InitContainer.ProtoReflect.Descriptor instead. func (*InitContainer) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{14} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{13} } func (x *InitContainer) GetName() string { @@ -1260,7 +1201,7 @@ type ImportContainer struct { func (x *ImportContainer) Reset() { *x = ImportContainer{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[15] + mi := &file_protobuf_proto_agent_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1273,7 +1214,7 @@ func (x *ImportContainer) String() string { func (*ImportContainer) ProtoMessage() {} func (x *ImportContainer) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[15] + mi := &file_protobuf_proto_agent_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1286,7 +1227,7 @@ func (x *ImportContainer) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportContainer.ProtoReflect.Descriptor instead. func (*ImportContainer) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{15} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{14} } func (x *ImportContainer) GetVolume() string { @@ -1322,7 +1263,7 @@ type LogConfig struct { func (x *LogConfig) Reset() { *x = LogConfig{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[16] + mi := &file_protobuf_proto_agent_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1335,7 +1276,7 @@ func (x *LogConfig) String() string { func (*LogConfig) ProtoMessage() {} func (x *LogConfig) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[16] + mi := &file_protobuf_proto_agent_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1348,7 +1289,7 @@ func (x *LogConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use LogConfig.ProtoReflect.Descriptor instead. func (*LogConfig) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{16} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{15} } func (x *LogConfig) GetDriver() common.DriverType { @@ -1378,7 +1319,7 @@ type Marker struct { func (x *Marker) Reset() { *x = Marker{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[17] + mi := &file_protobuf_proto_agent_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1391,7 +1332,7 @@ func (x *Marker) String() string { func (*Marker) ProtoMessage() {} func (x *Marker) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[17] + mi := &file_protobuf_proto_agent_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1404,7 +1345,7 @@ func (x *Marker) ProtoReflect() protoreflect.Message { // Deprecated: Use Marker.ProtoReflect.Descriptor instead. func (*Marker) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{17} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{16} } func (x *Marker) GetDeployment() map[string]string { @@ -1440,7 +1381,7 @@ type Metrics struct { func (x *Metrics) Reset() { *x = Metrics{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[18] + mi := &file_protobuf_proto_agent_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1453,7 +1394,7 @@ func (x *Metrics) String() string { func (*Metrics) ProtoMessage() {} func (x *Metrics) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[18] + mi := &file_protobuf_proto_agent_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1466,7 +1407,7 @@ func (x *Metrics) ProtoReflect() protoreflect.Message { // Deprecated: Use Metrics.ProtoReflect.Descriptor instead. func (*Metrics) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{18} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{17} } func (x *Metrics) GetPort() int32 { @@ -1496,7 +1437,7 @@ type ExpectedState struct { func (x *ExpectedState) Reset() { *x = ExpectedState{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[19] + mi := &file_protobuf_proto_agent_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1509,7 +1450,7 @@ func (x *ExpectedState) String() string { func (*ExpectedState) ProtoMessage() {} func (x *ExpectedState) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[19] + mi := &file_protobuf_proto_agent_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1522,7 +1463,7 @@ func (x *ExpectedState) ProtoReflect() protoreflect.Message { // Deprecated: Use ExpectedState.ProtoReflect.Descriptor instead. func (*ExpectedState) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{19} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{18} } func (x *ExpectedState) GetState() common.ContainerState { @@ -1562,7 +1503,7 @@ type DagentContainerConfig struct { func (x *DagentContainerConfig) Reset() { *x = DagentContainerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[20] + mi := &file_protobuf_proto_agent_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1575,7 +1516,7 @@ func (x *DagentContainerConfig) String() string { func (*DagentContainerConfig) ProtoMessage() {} func (x *DagentContainerConfig) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[20] + mi := &file_protobuf_proto_agent_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1588,7 +1529,7 @@ func (x *DagentContainerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DagentContainerConfig.ProtoReflect.Descriptor instead. func (*DagentContainerConfig) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{20} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{19} } func (x *DagentContainerConfig) GetLogConfig() *LogConfig { @@ -1653,7 +1594,7 @@ type CraneContainerConfig struct { func (x *CraneContainerConfig) Reset() { *x = CraneContainerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[21] + mi := &file_protobuf_proto_agent_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1666,7 +1607,7 @@ func (x *CraneContainerConfig) String() string { func (*CraneContainerConfig) ProtoMessage() {} func (x *CraneContainerConfig) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[21] + mi := &file_protobuf_proto_agent_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1679,7 +1620,7 @@ func (x *CraneContainerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use CraneContainerConfig.ProtoReflect.Descriptor instead. func (*CraneContainerConfig) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{21} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{20} } func (x *CraneContainerConfig) GetDeploymentStrategy() common.DeploymentStrategy { @@ -1778,7 +1719,7 @@ type CommonContainerConfig struct { func (x *CommonContainerConfig) Reset() { *x = CommonContainerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[22] + mi := &file_protobuf_proto_agent_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1791,7 +1732,7 @@ func (x *CommonContainerConfig) String() string { func (*CommonContainerConfig) ProtoMessage() {} func (x *CommonContainerConfig) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[22] + mi := &file_protobuf_proto_agent_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1804,7 +1745,7 @@ func (x *CommonContainerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use CommonContainerConfig.ProtoReflect.Descriptor instead. func (*CommonContainerConfig) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{22} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{21} } func (x *CommonContainerConfig) GetName() string { @@ -1919,44 +1860,39 @@ func (x *CommonContainerConfig) GetInitContainers() []*InitContainer { return nil } -type DeployRequest struct { +type DeployWorkloadRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - ContainerName string `protobuf:"bytes,2,opt,name=containerName,proto3" json:"containerName,omitempty"` - // InstanceConfig is set for multiple containers - InstanceConfig *InstanceConfig `protobuf:"bytes,3,opt,name=instanceConfig,proto3" json:"instanceConfig,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // ContainerConfigs - Common *CommonContainerConfig `protobuf:"bytes,4,opt,name=common,proto3,oneof" json:"common,omitempty"` - Dagent *DagentContainerConfig `protobuf:"bytes,5,opt,name=dagent,proto3,oneof" json:"dagent,omitempty"` - Crane *CraneContainerConfig `protobuf:"bytes,6,opt,name=crane,proto3,oneof" json:"crane,omitempty"` - // Runtime info and requirements of a container - RuntimeConfig *string `protobuf:"bytes,7,opt,name=runtimeConfig,proto3,oneof" json:"runtimeConfig,omitempty"` - Registry *string `protobuf:"bytes,8,opt,name=registry,proto3,oneof" json:"registry,omitempty"` - ImageName string `protobuf:"bytes,9,opt,name=imageName,proto3" json:"imageName,omitempty"` - Tag string `protobuf:"bytes,10,opt,name=tag,proto3" json:"tag,omitempty"` - RegistryAuth *RegistryAuth `protobuf:"bytes,11,opt,name=registryAuth,proto3,oneof" json:"registryAuth,omitempty"` + Common *CommonContainerConfig `protobuf:"bytes,2,opt,name=common,proto3,oneof" json:"common,omitempty"` + Dagent *DagentContainerConfig `protobuf:"bytes,3,opt,name=dagent,proto3,oneof" json:"dagent,omitempty"` + Crane *CraneContainerConfig `protobuf:"bytes,4,opt,name=crane,proto3,oneof" json:"crane,omitempty"` + Registry *string `protobuf:"bytes,5,opt,name=registry,proto3,oneof" json:"registry,omitempty"` + ImageName string `protobuf:"bytes,6,opt,name=imageName,proto3" json:"imageName,omitempty"` + Tag string `protobuf:"bytes,7,opt,name=tag,proto3" json:"tag,omitempty"` + RegistryAuth *RegistryAuth `protobuf:"bytes,8,opt,name=registryAuth,proto3,oneof" json:"registryAuth,omitempty"` } -func (x *DeployRequest) Reset() { - *x = DeployRequest{} +func (x *DeployWorkloadRequest) Reset() { + *x = DeployWorkloadRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[23] + mi := &file_protobuf_proto_agent_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *DeployRequest) String() string { +func (x *DeployWorkloadRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeployRequest) ProtoMessage() {} +func (*DeployWorkloadRequest) ProtoMessage() {} -func (x *DeployRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[23] +func (x *DeployWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_proto_agent_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1967,82 +1903,61 @@ func (x *DeployRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeployRequest.ProtoReflect.Descriptor instead. -func (*DeployRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{23} +// Deprecated: Use DeployWorkloadRequest.ProtoReflect.Descriptor instead. +func (*DeployWorkloadRequest) Descriptor() ([]byte, []int) { + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{22} } -func (x *DeployRequest) GetId() string { +func (x *DeployWorkloadRequest) GetId() string { if x != nil { return x.Id } return "" } -func (x *DeployRequest) GetContainerName() string { - if x != nil { - return x.ContainerName - } - return "" -} - -func (x *DeployRequest) GetInstanceConfig() *InstanceConfig { - if x != nil { - return x.InstanceConfig - } - return nil -} - -func (x *DeployRequest) GetCommon() *CommonContainerConfig { +func (x *DeployWorkloadRequest) GetCommon() *CommonContainerConfig { if x != nil { return x.Common } return nil } -func (x *DeployRequest) GetDagent() *DagentContainerConfig { +func (x *DeployWorkloadRequest) GetDagent() *DagentContainerConfig { if x != nil { return x.Dagent } return nil } -func (x *DeployRequest) GetCrane() *CraneContainerConfig { +func (x *DeployWorkloadRequest) GetCrane() *CraneContainerConfig { if x != nil { return x.Crane } return nil } -func (x *DeployRequest) GetRuntimeConfig() string { - if x != nil && x.RuntimeConfig != nil { - return *x.RuntimeConfig - } - return "" -} - -func (x *DeployRequest) GetRegistry() string { +func (x *DeployWorkloadRequest) GetRegistry() string { if x != nil && x.Registry != nil { return *x.Registry } return "" } -func (x *DeployRequest) GetImageName() string { +func (x *DeployWorkloadRequest) GetImageName() string { if x != nil { return x.ImageName } return "" } -func (x *DeployRequest) GetTag() string { +func (x *DeployWorkloadRequest) GetTag() string { if x != nil { return x.Tag } return "" } -func (x *DeployRequest) GetRegistryAuth() *RegistryAuth { +func (x *DeployWorkloadRequest) GetRegistryAuth() *RegistryAuth { if x != nil { return x.RegistryAuth } @@ -2061,7 +1976,7 @@ type ContainerStateRequest struct { func (x *ContainerStateRequest) Reset() { *x = ContainerStateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[24] + mi := &file_protobuf_proto_agent_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2074,7 +1989,7 @@ func (x *ContainerStateRequest) String() string { func (*ContainerStateRequest) ProtoMessage() {} func (x *ContainerStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[24] + mi := &file_protobuf_proto_agent_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2087,7 +2002,7 @@ func (x *ContainerStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerStateRequest.ProtoReflect.Descriptor instead. func (*ContainerStateRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{24} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{23} } func (x *ContainerStateRequest) GetPrefix() string { @@ -2116,7 +2031,7 @@ type ContainerDeleteRequest struct { func (x *ContainerDeleteRequest) Reset() { *x = ContainerDeleteRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[25] + mi := &file_protobuf_proto_agent_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2129,7 +2044,7 @@ func (x *ContainerDeleteRequest) String() string { func (*ContainerDeleteRequest) ProtoMessage() {} func (x *ContainerDeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[25] + mi := &file_protobuf_proto_agent_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2142,7 +2057,7 @@ func (x *ContainerDeleteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerDeleteRequest.ProtoReflect.Descriptor instead. func (*ContainerDeleteRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{25} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{24} } func (x *ContainerDeleteRequest) GetPrefix() string { @@ -2171,7 +2086,7 @@ type DeployRequestLegacy struct { func (x *DeployRequestLegacy) Reset() { *x = DeployRequestLegacy{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[26] + mi := &file_protobuf_proto_agent_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2184,7 +2099,7 @@ func (x *DeployRequestLegacy) String() string { func (*DeployRequestLegacy) ProtoMessage() {} func (x *DeployRequestLegacy) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[26] + mi := &file_protobuf_proto_agent_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2197,7 +2112,7 @@ func (x *DeployRequestLegacy) ProtoReflect() protoreflect.Message { // Deprecated: Use DeployRequestLegacy.ProtoReflect.Descriptor instead. func (*DeployRequestLegacy) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{26} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{25} } func (x *DeployRequestLegacy) GetRequestId() string { @@ -2228,7 +2143,7 @@ type AgentUpdateRequest struct { func (x *AgentUpdateRequest) Reset() { *x = AgentUpdateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[27] + mi := &file_protobuf_proto_agent_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2241,7 +2156,7 @@ func (x *AgentUpdateRequest) String() string { func (*AgentUpdateRequest) ProtoMessage() {} func (x *AgentUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[27] + mi := &file_protobuf_proto_agent_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2254,7 +2169,7 @@ func (x *AgentUpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentUpdateRequest.ProtoReflect.Descriptor instead. func (*AgentUpdateRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{27} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{26} } func (x *AgentUpdateRequest) GetTag() string { @@ -2289,7 +2204,7 @@ type ReplaceTokenRequest struct { func (x *ReplaceTokenRequest) Reset() { *x = ReplaceTokenRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[28] + mi := &file_protobuf_proto_agent_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2302,7 +2217,7 @@ func (x *ReplaceTokenRequest) String() string { func (*ReplaceTokenRequest) ProtoMessage() {} func (x *ReplaceTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[28] + mi := &file_protobuf_proto_agent_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2315,7 +2230,7 @@ func (x *ReplaceTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReplaceTokenRequest.ProtoReflect.Descriptor instead. func (*ReplaceTokenRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{28} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{27} } func (x *ReplaceTokenRequest) GetToken() string { @@ -2336,7 +2251,7 @@ type AgentAbortUpdate struct { func (x *AgentAbortUpdate) Reset() { *x = AgentAbortUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[29] + mi := &file_protobuf_proto_agent_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2349,7 +2264,7 @@ func (x *AgentAbortUpdate) String() string { func (*AgentAbortUpdate) ProtoMessage() {} func (x *AgentAbortUpdate) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[29] + mi := &file_protobuf_proto_agent_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2362,7 +2277,7 @@ func (x *AgentAbortUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentAbortUpdate.ProtoReflect.Descriptor instead. func (*AgentAbortUpdate) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{29} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{28} } func (x *AgentAbortUpdate) GetError() string { @@ -2386,7 +2301,7 @@ type ContainerLogRequest struct { func (x *ContainerLogRequest) Reset() { *x = ContainerLogRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[30] + mi := &file_protobuf_proto_agent_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2399,7 +2314,7 @@ func (x *ContainerLogRequest) String() string { func (*ContainerLogRequest) ProtoMessage() {} func (x *ContainerLogRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[30] + mi := &file_protobuf_proto_agent_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2412,7 +2327,7 @@ func (x *ContainerLogRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerLogRequest.ProtoReflect.Descriptor instead. func (*ContainerLogRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{30} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{29} } func (x *ContainerLogRequest) GetContainer() *common.ContainerIdentifier { @@ -2448,7 +2363,7 @@ type ContainerInspectRequest struct { func (x *ContainerInspectRequest) Reset() { *x = ContainerInspectRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[31] + mi := &file_protobuf_proto_agent_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2461,7 +2376,7 @@ func (x *ContainerInspectRequest) String() string { func (*ContainerInspectRequest) ProtoMessage() {} func (x *ContainerInspectRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[31] + mi := &file_protobuf_proto_agent_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2474,7 +2389,7 @@ func (x *ContainerInspectRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerInspectRequest.ProtoReflect.Descriptor instead. func (*ContainerInspectRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{31} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{30} } func (x *ContainerInspectRequest) GetContainer() *common.ContainerIdentifier { @@ -2495,7 +2410,7 @@ type CloseConnectionRequest struct { func (x *CloseConnectionRequest) Reset() { *x = CloseConnectionRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[32] + mi := &file_protobuf_proto_agent_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2508,7 +2423,7 @@ func (x *CloseConnectionRequest) String() string { func (*CloseConnectionRequest) ProtoMessage() {} func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[32] + mi := &file_protobuf_proto_agent_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2521,7 +2436,7 @@ func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead. func (*CloseConnectionRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{32} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{31} } func (x *CloseConnectionRequest) GetReason() CloseReason { @@ -2547,515 +2462,495 @@ var file_protobuf_proto_agent_proto_rawDesc = []byte{ 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, - 0x61, 0x6d, 0x65, 0x22, 0xc0, 0x06, 0x0a, 0x0c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x35, 0x0a, 0x06, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x48, 0x00, 0x52, 0x06, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x12, 0x46, 0x0a, 0x0e, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x48, 0x00, 0x52, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x40, - 0x0a, 0x0c, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x70, - 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, - 0x48, 0x00, 0x52, 0x0c, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, - 0x12, 0x3d, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, - 0x33, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x75, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x6f, 0x73, - 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x10, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x4d, 0x0a, 0x10, 0x64, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x40, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, + 0x61, 0x6d, 0x65, 0x22, 0xb9, 0x06, 0x0a, 0x0c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x2e, 0x0a, 0x06, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x70, + 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x64, 0x65, + 0x70, 0x6c, 0x6f, 0x79, 0x12, 0x46, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x0f, + 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x40, 0x0a, 0x0c, 0x64, 0x65, 0x70, 0x6c, 0x6f, + 0x79, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x48, 0x00, 0x52, 0x0c, 0x64, 0x65, 0x70, + 0x6c, 0x6f, 0x79, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x3d, 0x0a, 0x0b, 0x6c, 0x69, 0x73, + 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x73, + 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, + 0x05, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x63, + 0x6c, 0x6f, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, + 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x12, 0x4d, 0x0a, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x73, 0x12, 0x40, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, + 0x6f, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x4c, 0x6f, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x4c, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x48, 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, + 0x70, 0x65, 0x63, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, + 0x3a, 0x0a, 0x0a, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x90, 0x02, 0x0a, 0x11, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x73, + 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x3f, 0x0a, 0x10, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x37, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x72, - 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, - 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x4c, 0x0a, - 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, - 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x63, - 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x3a, 0x0a, 0x0a, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x22, 0x90, 0x02, 0x0a, 0x11, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, - 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, - 0x3f, 0x0a, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x10, - 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, - 0x12, 0x37, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x12, 0x3f, 0x0a, 0x10, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x2a, 0x0a, 0x0e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, - 0x64, 0x22, 0x9e, 0x01, 0x0a, 0x14, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x70, - 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, - 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x73, - 0x12, 0x30, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x73, 0x22, 0x4f, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x22, 0xa9, 0x02, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x21, - 0x0a, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x88, 0x01, - 0x01, 0x12, 0x48, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, - 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x6e, - 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, - 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x2f, 0x0a, 0x10, 0x72, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x10, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, - 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x88, 0x01, 0x01, 0x1a, 0x3e, 0x0a, 0x10, - 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0c, 0x0a, 0x0a, - 0x5f, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, - 0x64, 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x50, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, - 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x64, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x1f, 0x0a, 0x08, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0x2f, 0x0a, 0x09, 0x50, 0x6f, 0x72, 0x74, 0x52, - 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x64, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x65, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x74, 0x6f, 0x22, 0x6e, 0x0a, 0x10, 0x50, 0x6f, 0x72, 0x74, - 0x52, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x2c, 0x0a, 0x08, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, - 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x2c, 0x0a, 0x08, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x08, - 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0xad, 0x01, 0x0a, 0x06, 0x56, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, - 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x17, 0x0a, 0x04, 0x73, - 0x69, 0x7a, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x73, 0x69, 0x7a, - 0x65, 0x88, 0x01, 0x01, 0x12, 0x2b, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x67, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x56, 0x6f, 0x6c, 0x75, - 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x48, 0x01, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x88, 0x01, - 0x01, 0x12, 0x19, 0x0a, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x68, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x02, 0x52, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x88, 0x01, 0x01, 0x42, 0x07, 0x0a, 0x05, - 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x08, - 0x0a, 0x06, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x22, 0x34, 0x0a, 0x0a, 0x56, 0x6f, 0x6c, 0x75, - 0x6d, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, - 0x74, 0x68, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0xe4, - 0x02, 0x0a, 0x0d, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x65, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x2d, 0x0a, 0x0f, 0x75, 0x73, - 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x66, 0x20, - 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0f, 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x2c, 0x0a, 0x07, 0x76, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x07, - 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x12, 0x13, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0xea, 0x07, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x12, 0x48, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, - 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0xeb, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, - 0x74, 0x1a, 0x3e, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xcf, 0x01, 0x0a, 0x0f, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x65, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x4a, 0x0a, 0x0b, 0x65, - 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, - 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, - 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x3e, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, 0x72, - 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xad, 0x01, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x18, - 0x64, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, - 0x72, 0x69, 0x76, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, - 0x72, 0x12, 0x38, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xe8, 0x07, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x6f, 0x67, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xed, 0x02, 0x0a, 0x06, 0x4d, 0x61, 0x72, 0x6b, - 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x12, 0x35, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0xe9, 0x07, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, - 0x6b, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x35, 0x0a, 0x07, 0x69, 0x6e, 0x67, - 0x72, 0x65, 0x73, 0x73, 0x18, 0xea, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, - 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, - 0x1a, 0x3d, 0x0a, 0x0f, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, + 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x48, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, + 0x6f, 0x67, 0x12, 0x3f, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, + 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, + 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, + 0x65, 0x63, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x2a, + 0x0a, 0x0e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x22, 0xb0, 0x02, 0x0a, 0x0d, 0x44, + 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, + 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, + 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x3b, 0x0a, 0x07, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x47, 0x0a, + 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x72, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0x64, 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, + 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, + 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x50, 0x0a, 0x04, + 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x18, 0x64, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x12, 0x1f, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x65, 0x20, 0x01, + 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x88, 0x01, + 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0x2f, + 0x0a, 0x09, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, + 0x72, 0x6f, 0x6d, 0x18, 0x64, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, + 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x74, 0x6f, 0x22, + 0x6e, 0x0a, 0x10, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x69, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x12, 0x2c, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, + 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x12, 0x2c, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x65, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, + 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, + 0xad, 0x01, 0x0a, 0x06, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x12, 0x17, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2b, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x48, 0x01, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x63, 0x6c, 0x61, 0x73, + 0x73, 0x18, 0x68, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, + 0x88, 0x01, 0x01, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x07, 0x0a, 0x05, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x22, + 0x34, 0x0a, 0x0a, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0xe4, 0x02, 0x0a, 0x0d, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, + 0x6d, 0x61, 0x67, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, + 0x65, 0x12, 0x2d, 0x0a, 0x0f, 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x66, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0f, 0x75, 0x73, + 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, + 0x12, 0x2c, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, + 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x19, + 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x13, 0x0a, 0x04, 0x61, 0x72, 0x67, + 0x73, 0x18, 0xea, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x12, 0x48, + 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0xeb, 0x07, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6e, 0x69, + 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, + 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x65, 0x6e, 0x76, + 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x3e, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x75, 0x73, 0x65, + 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xcf, 0x01, 0x0a, + 0x0f, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x12, 0x4a, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x3e, + 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xad, + 0x01, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, 0x06, + 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x72, 0x69, 0x76, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, + 0x52, 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x4c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xed, + 0x02, 0x0a, 0x06, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x0a, 0x64, 0x65, 0x70, + 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x44, 0x65, + 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x64, + 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x35, 0x0a, 0x07, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x35, 0x0a, 0x07, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0xea, 0x07, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, + 0x72, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, + 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x1a, 0x3d, 0x0a, 0x0f, 0x44, 0x65, 0x70, 0x6c, 0x6f, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x31, + 0x0a, 0x07, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, + 0x68, 0x22, 0x96, 0x01, 0x0a, 0x0d, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x65, 0x20, 0x01, + 0x28, 0x05, 0x48, 0x00, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x88, 0x01, 0x01, + 0x12, 0x1f, 0x0a, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x66, 0x20, 0x01, + 0x28, 0x05, 0x48, 0x01, 0x52, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, + 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x42, 0x0b, 0x0a, + 0x09, 0x5f, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x22, 0xe8, 0x03, 0x0a, 0x15, 0x44, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x33, 0x0a, 0x09, 0x6c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x4c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x09, 0x6c, 0x6f, 0x67, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x0d, 0x72, 0x65, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x48, 0x01, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x88, 0x01, 0x01, 0x12, 0x3a, 0x0a, 0x0b, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x4d, 0x6f, 0x64, 0x65, 0x48, 0x02, 0x52, 0x0b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x4d, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x12, 0x3f, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x48, 0x03, 0x52, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x41, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, + 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, + 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x6c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x6f, 0x64, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0xc3, 0x06, 0x0a, 0x14, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, + 0x0a, 0x12, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, + 0x74, 0x65, 0x67, 0x79, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x48, 0x00, 0x52, 0x12, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x88, 0x01, 0x01, 0x12, + 0x4c, 0x0a, 0x11, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x01, 0x52, 0x11, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x43, 0x0a, + 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x02, 0x52, + 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, + 0x01, 0x01, 0x12, 0x27, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x73, 0x18, 0x67, 0x20, 0x01, 0x28, 0x08, 0x48, 0x03, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x0f, 0x75, + 0x73, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x18, 0x68, + 0x20, 0x01, 0x28, 0x08, 0x48, 0x04, 0x52, 0x0f, 0x75, 0x73, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, + 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x34, 0x0a, 0x0b, 0x61, 0x6e, + 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x48, 0x05, + 0x52, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, + 0x12, 0x2a, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x6a, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x48, + 0x06, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x07, + 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, 0x6b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x48, 0x07, 0x52, + 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0d, 0x63, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0xe8, 0x07, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x73, 0x12, 0x64, 0x0a, 0x12, 0x65, 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x33, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x78, 0x74, + 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x65, 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x45, 0x0a, 0x17, 0x45, 0x78, 0x74, 0x72, + 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, + 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x11, 0x0a, 0x0f, + 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, + 0x0f, 0x0a, 0x0d, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, + 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x75, 0x73, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, + 0x6e, 0x63, 0x65, 0x72, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x42, + 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x22, 0xf2, 0x07, 0x0a, 0x15, + 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x65, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x65, 0x78, 0x70, + 0x6f, 0x73, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, + 0x79, 0x48, 0x00, 0x52, 0x06, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2e, + 0x0a, 0x07, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, + 0x48, 0x01, 0x52, 0x07, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x88, 0x01, 0x01, 0x12, 0x46, + 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x48, 0x02, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, 0x0f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x16, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x48, 0x03, 0x52, 0x0f, 0x69, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x6a, 0x20, 0x01, 0x28, 0x03, 0x48, 0x04, 0x52, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x15, 0x0a, 0x03, 0x54, 0x54, 0x59, 0x18, 0x6b, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x03, 0x54, 0x54, 0x59, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, + 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x79, 0x18, 0x6c, 0x20, 0x01, 0x28, 0x09, 0x48, 0x06, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x69, + 0x6e, 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x22, + 0x0a, 0x05, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x05, 0x70, 0x6f, 0x72, + 0x74, 0x73, 0x12, 0x38, 0x0a, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, + 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x07, + 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0xea, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x07, 0x76, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x73, 0x18, 0xeb, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x73, 0x12, 0x13, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0xec, 0x07, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x12, 0x50, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0xed, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x6e, 0x76, + 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x65, + 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x07, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0xee, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, + 0x12, 0x3d, 0x0a, 0x0e, 0x69, 0x6e, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x73, 0x18, 0xef, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, + 0x0e, 0x69, 0x6e, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a, + 0x3e, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, - 0x3a, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x3a, 0x0a, 0x0c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x49, - 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x31, 0x0a, 0x07, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x96, 0x01, 0x0a, 0x0d, 0x45, - 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2c, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x74, 0x69, - 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x07, 0x74, - 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x65, 0x78, 0x69, - 0x74, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x05, 0x48, 0x01, 0x52, 0x08, 0x65, - 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x65, 0x78, 0x69, 0x74, 0x43, - 0x6f, 0x64, 0x65, 0x22, 0xe8, 0x03, 0x0a, 0x15, 0x44, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x33, 0x0a, - 0x09, 0x6c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x48, 0x00, 0x52, 0x09, 0x6c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, - 0x01, 0x01, 0x12, 0x40, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x48, 0x01, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x88, 0x01, 0x01, 0x12, 0x3a, 0x0a, 0x0b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, - 0x6f, 0x64, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x64, 0x65, 0x48, 0x02, - 0x52, 0x0b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, - 0x12, 0x3f, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x48, 0x03, 0x52, - 0x0d, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x88, 0x01, - 0x01, 0x12, 0x1b, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0xe8, 0x07, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x41, - 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x28, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4c, 0x61, - 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, - 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0c, 0x0a, 0x0a, - 0x5f, 0x6c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, - 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x0e, 0x0a, 0x0c, - 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x10, 0x0a, 0x0e, - 0x5f, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0xc3, - 0x06, 0x0a, 0x14, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, 0x0a, 0x12, 0x64, 0x65, 0x70, 0x6c, 0x6f, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x64, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x70, - 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x48, - 0x00, 0x52, 0x12, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, - 0x61, 0x74, 0x65, 0x67, 0x79, 0x88, 0x01, 0x01, 0x12, 0x4c, 0x0a, 0x11, 0x68, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x65, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x48, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x01, - 0x52, 0x11, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x43, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x02, 0x52, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x27, 0x0a, 0x0c, 0x70, - 0x72, 0x6f, 0x78, 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x67, 0x20, 0x01, 0x28, - 0x08, 0x48, 0x03, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x73, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x0f, 0x75, 0x73, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, - 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x18, 0x68, 0x20, 0x01, 0x28, 0x08, 0x48, 0x04, 0x52, - 0x0f, 0x75, 0x73, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, - 0x88, 0x01, 0x01, 0x12, 0x34, 0x0a, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x48, 0x05, 0x52, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2a, 0x0a, 0x06, 0x6c, 0x61, 0x62, - 0x65, 0x6c, 0x73, 0x18, 0x6a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x48, 0x06, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, - 0x6c, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, - 0x18, 0x6b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x48, 0x07, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x73, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, - 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x64, 0x0a, 0x12, 0x65, - 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, - 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x65, - 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x1a, 0x45, 0x0a, 0x17, 0x45, 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, 0x6f, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x65, 0x70, - 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x42, - 0x14, 0x0a, 0x12, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x70, 0x72, 0x6f, - 0x78, 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x75, 0x73, - 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x42, 0x0e, 0x0a, - 0x0c, 0x5f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x09, 0x0a, - 0x07, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x74, - 0x72, 0x69, 0x63, 0x73, 0x22, 0xf2, 0x07, 0x0a, 0x15, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x66, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x78, 0x70, 0x6f, - 0x73, 0x65, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x48, 0x00, 0x52, 0x06, 0x65, 0x78, - 0x70, 0x6f, 0x73, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x07, 0x72, 0x6f, 0x75, 0x74, 0x69, - 0x6e, 0x67, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x48, 0x01, 0x52, 0x07, 0x72, 0x6f, 0x75, - 0x74, 0x69, 0x6e, 0x67, 0x88, 0x01, 0x01, 0x12, 0x46, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x48, 0x02, 0x52, 0x0f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, - 0x45, 0x0a, 0x0f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x48, 0x03, 0x52, 0x0f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x6a, - 0x20, 0x01, 0x28, 0x03, 0x48, 0x04, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, - 0x15, 0x0a, 0x03, 0x54, 0x54, 0x59, 0x18, 0x6b, 0x20, 0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x03, - 0x54, 0x54, 0x59, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, - 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x6c, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x06, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x05, 0x70, 0x6f, 0x72, 0x74, 0x73, - 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x6f, 0x72, 0x74, 0x52, 0x05, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x38, 0x0a, 0x0a, 0x70, - 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, - 0x67, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x52, - 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, - 0x18, 0xea, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, - 0x1b, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x18, 0xeb, 0x07, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x12, 0x13, 0x0a, 0x04, - 0x61, 0x72, 0x67, 0x73, 0x18, 0xec, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, - 0x73, 0x12, 0x50, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, - 0x18, 0xed, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, - 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, - 0x65, 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x07, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0xee, - 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x07, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x3d, 0x0a, 0x0e, 0x69, 0x6e, 0x69, - 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0xef, 0x07, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0e, 0x69, 0x6e, 0x69, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x3e, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, - 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x42, - 0x0a, 0x0a, 0x08, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x12, 0x0a, 0x10, 0x5f, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x42, - 0x12, 0x0a, 0x10, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x42, 0x06, 0x0a, 0x04, - 0x5f, 0x54, 0x54, 0x59, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, - 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x22, 0xbc, 0x04, 0x0a, 0x0d, 0x44, 0x65, - 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x3d, 0x0a, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x39, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, - 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x06, 0x64, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x01, 0x52, 0x06, 0x64, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x36, 0x0a, 0x05, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x72, - 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x48, 0x02, 0x52, 0x05, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x88, 0x01, 0x01, 0x12, 0x29, - 0x0a, 0x0d, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x0d, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x04, 0x52, 0x08, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6d, - 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, - 0x6d, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x3c, 0x0a, 0x0c, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x41, 0x75, 0x74, 0x68, 0x48, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x41, 0x75, 0x74, 0x68, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x64, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x42, 0x08, - 0x0a, 0x06, 0x5f, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x75, 0x6e, - 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x22, 0x6a, 0x0a, 0x15, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x1b, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x88, 0x01, 0x01, 0x12, 0x1d, - 0x0a, 0x07, 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, - 0x01, 0x52, 0x07, 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, - 0x07, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6f, 0x6e, 0x65, - 0x53, 0x68, 0x6f, 0x74, 0x22, 0x44, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, - 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x47, 0x0a, 0x13, 0x44, 0x65, - 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x65, 0x67, 0x61, 0x63, - 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6a, - 0x73, 0x6f, 0x6e, 0x22, 0x64, 0x0a, 0x12, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x74, - 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, - 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x2b, 0x0a, 0x13, 0x52, 0x65, 0x70, - 0x6c, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x28, 0x0a, 0x10, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, - 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x22, 0x82, 0x01, 0x0a, 0x13, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, - 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, - 0x67, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x04, 0x74, 0x61, 0x69, 0x6c, 0x22, 0x54, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x44, 0x0a, 0x16, 0x43, - 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, - 0x6f, 0x73, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, - 0x6e, 0x2a, 0x69, 0x0a, 0x0b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, - 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x45, 0x4c, - 0x46, 0x5f, 0x44, 0x45, 0x53, 0x54, 0x52, 0x55, 0x43, 0x54, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, - 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, - 0x56, 0x4f, 0x4b, 0x45, 0x5f, 0x54, 0x4f, 0x4b, 0x45, 0x4e, 0x10, 0x04, 0x32, 0x9c, 0x05, 0x0a, - 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x12, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x1a, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x30, 0x01, 0x12, 0x37, 0x0a, 0x0c, 0x43, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x35, 0x0a, 0x0b, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x12, 0x17, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, - 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2d, 0x0a, 0x0d, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x64, 0x12, 0x0d, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x10, 0x44, 0x65, 0x70, - 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, - 0x44, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x21, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x42, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1b, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, - 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x38, 0x0a, 0x0a, 0x53, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, + 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x69, + 0x6e, 0x67, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x75, + 0x73, 0x65, 0x72, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x54, 0x54, 0x59, 0x42, 0x13, 0x0a, 0x11, 0x5f, + 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, + 0x22, 0xa2, 0x03, 0x0a, 0x15, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x57, 0x6f, 0x72, 0x6b, 0x6c, + 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x06, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x06, 0x64, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x48, 0x01, 0x52, 0x06, 0x64, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, + 0x12, 0x36, 0x0a, 0x05, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x02, 0x52, 0x05, + 0x63, 0x72, 0x61, 0x6e, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x08, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6d, 0x61, + 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6d, + 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x3c, 0x0a, 0x0c, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, + 0x41, 0x75, 0x74, 0x68, 0x48, 0x04, 0x52, 0x0c, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, + 0x41, 0x75, 0x74, 0x68, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x64, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x42, 0x08, 0x0a, + 0x06, 0x5f, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x41, 0x75, 0x74, 0x68, 0x22, 0x6a, 0x0a, 0x15, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, + 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, + 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x6f, + 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x07, + 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, + 0x74, 0x22, 0x44, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, + 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x47, 0x0a, 0x13, 0x44, 0x65, 0x70, 0x6c, 0x6f, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x1c, + 0x0a, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, + 0x22, 0x64, 0x0a, 0x12, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x69, 0x6d, 0x65, + 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, + 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x2b, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x22, 0x28, 0x0a, 0x10, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x62, 0x6f, 0x72, + 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x82, 0x01, + 0x0a, 0x13, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x74, 0x61, + 0x69, 0x6c, 0x22, 0x54, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, + 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, + 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x44, 0x0a, 0x16, 0x43, 0x6c, 0x6f, 0x73, + 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, + 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x2a, 0x69, + 0x0a, 0x0b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, + 0x18, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x43, + 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x45, 0x4c, 0x46, 0x5f, 0x44, + 0x45, 0x53, 0x54, 0x52, 0x55, 0x43, 0x54, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x48, 0x55, + 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x56, 0x4f, 0x4b, + 0x45, 0x5f, 0x54, 0x4f, 0x4b, 0x45, 0x4e, 0x10, 0x04, 0x32, 0x9c, 0x05, 0x0a, 0x05, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x10, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x1a, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x30, 0x01, 0x12, 0x37, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x35, 0x0a, 0x0b, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, + 0x17, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x62, 0x6f, + 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2d, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x64, 0x12, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x10, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x44, 0x0a, 0x0e, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x28, 0x01, 0x12, 0x42, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, + 0x6f, 0x67, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x38, 0x0a, 0x0a, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, + 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x30, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x73, 0x12, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, + 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x30, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, - 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x35, 0x5a, 0x33, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x2d, 0x69, 0x6f, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x69, 0x6f, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, 0x2f, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2d, + 0x69, 0x6f, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x69, 0x6f, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3071,7 +2966,7 @@ func file_protobuf_proto_agent_proto_rawDescGZIP() []byte { } var file_protobuf_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_protobuf_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 44) +var file_protobuf_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 43) var file_protobuf_proto_agent_proto_goTypes = []interface{}{ (CloseReason)(0), // 0: agent.CloseReason (*AgentInfo)(nil), // 1: agent.AgentInfo @@ -3079,59 +2974,59 @@ var file_protobuf_proto_agent_proto_goTypes = []interface{}{ (*AgentError)(nil), // 3: agent.AgentError (*AgentCommandError)(nil), // 4: agent.AgentCommandError (*DeployResponse)(nil), // 5: agent.DeployResponse - (*VersionDeployRequest)(nil), // 6: agent.VersionDeployRequest + (*DeployRequest)(nil), // 6: agent.DeployRequest (*ListSecretsRequest)(nil), // 7: agent.ListSecretsRequest - (*InstanceConfig)(nil), // 8: agent.InstanceConfig - (*RegistryAuth)(nil), // 9: agent.RegistryAuth - (*Port)(nil), // 10: agent.Port - (*PortRange)(nil), // 11: agent.PortRange - (*PortRangeBinding)(nil), // 12: agent.PortRangeBinding - (*Volume)(nil), // 13: agent.Volume - (*VolumeLink)(nil), // 14: agent.VolumeLink - (*InitContainer)(nil), // 15: agent.InitContainer - (*ImportContainer)(nil), // 16: agent.ImportContainer - (*LogConfig)(nil), // 17: agent.LogConfig - (*Marker)(nil), // 18: agent.Marker - (*Metrics)(nil), // 19: agent.Metrics - (*ExpectedState)(nil), // 20: agent.ExpectedState - (*DagentContainerConfig)(nil), // 21: agent.DagentContainerConfig - (*CraneContainerConfig)(nil), // 22: agent.CraneContainerConfig - (*CommonContainerConfig)(nil), // 23: agent.CommonContainerConfig - (*DeployRequest)(nil), // 24: agent.DeployRequest - (*ContainerStateRequest)(nil), // 25: agent.ContainerStateRequest - (*ContainerDeleteRequest)(nil), // 26: agent.ContainerDeleteRequest - (*DeployRequestLegacy)(nil), // 27: agent.DeployRequestLegacy - (*AgentUpdateRequest)(nil), // 28: agent.AgentUpdateRequest - (*ReplaceTokenRequest)(nil), // 29: agent.ReplaceTokenRequest - (*AgentAbortUpdate)(nil), // 30: agent.AgentAbortUpdate - (*ContainerLogRequest)(nil), // 31: agent.ContainerLogRequest - (*ContainerInspectRequest)(nil), // 32: agent.ContainerInspectRequest - (*CloseConnectionRequest)(nil), // 33: agent.CloseConnectionRequest - nil, // 34: agent.InstanceConfig.EnvironmentEntry - nil, // 35: agent.InitContainer.EnvironmentEntry - nil, // 36: agent.ImportContainer.EnvironmentEntry - nil, // 37: agent.LogConfig.OptionsEntry - nil, // 38: agent.Marker.DeploymentEntry - nil, // 39: agent.Marker.ServiceEntry - nil, // 40: agent.Marker.IngressEntry - nil, // 41: agent.DagentContainerConfig.LabelsEntry - nil, // 42: agent.CraneContainerConfig.ExtraLBAnnotationsEntry - nil, // 43: agent.CommonContainerConfig.EnvironmentEntry - nil, // 44: agent.CommonContainerConfig.SecretsEntry - (*common.ContainerCommandRequest)(nil), // 45: common.ContainerCommandRequest - (*common.DeleteContainersRequest)(nil), // 46: common.DeleteContainersRequest - (*common.ContainerIdentifier)(nil), // 47: common.ContainerIdentifier - (common.VolumeType)(0), // 48: common.VolumeType - (common.DriverType)(0), // 49: common.DriverType - (common.ContainerState)(0), // 50: common.ContainerState - (common.RestartPolicy)(0), // 51: common.RestartPolicy - (common.NetworkMode)(0), // 52: common.NetworkMode - (common.DeploymentStrategy)(0), // 53: common.DeploymentStrategy - (*common.HealthCheckConfig)(nil), // 54: common.HealthCheckConfig - (*common.ResourceConfig)(nil), // 55: common.ResourceConfig - (common.ExposeStrategy)(0), // 56: common.ExposeStrategy - (*common.Routing)(nil), // 57: common.Routing - (*common.ConfigContainer)(nil), // 58: common.ConfigContainer + (*RegistryAuth)(nil), // 8: agent.RegistryAuth + (*Port)(nil), // 9: agent.Port + (*PortRange)(nil), // 10: agent.PortRange + (*PortRangeBinding)(nil), // 11: agent.PortRangeBinding + (*Volume)(nil), // 12: agent.Volume + (*VolumeLink)(nil), // 13: agent.VolumeLink + (*InitContainer)(nil), // 14: agent.InitContainer + (*ImportContainer)(nil), // 15: agent.ImportContainer + (*LogConfig)(nil), // 16: agent.LogConfig + (*Marker)(nil), // 17: agent.Marker + (*Metrics)(nil), // 18: agent.Metrics + (*ExpectedState)(nil), // 19: agent.ExpectedState + (*DagentContainerConfig)(nil), // 20: agent.DagentContainerConfig + (*CraneContainerConfig)(nil), // 21: agent.CraneContainerConfig + (*CommonContainerConfig)(nil), // 22: agent.CommonContainerConfig + (*DeployWorkloadRequest)(nil), // 23: agent.DeployWorkloadRequest + (*ContainerStateRequest)(nil), // 24: agent.ContainerStateRequest + (*ContainerDeleteRequest)(nil), // 25: agent.ContainerDeleteRequest + (*DeployRequestLegacy)(nil), // 26: agent.DeployRequestLegacy + (*AgentUpdateRequest)(nil), // 27: agent.AgentUpdateRequest + (*ReplaceTokenRequest)(nil), // 28: agent.ReplaceTokenRequest + (*AgentAbortUpdate)(nil), // 29: agent.AgentAbortUpdate + (*ContainerLogRequest)(nil), // 30: agent.ContainerLogRequest + (*ContainerInspectRequest)(nil), // 31: agent.ContainerInspectRequest + (*CloseConnectionRequest)(nil), // 32: agent.CloseConnectionRequest + nil, // 33: agent.DeployRequest.SecretsEntry + nil, // 34: agent.InitContainer.EnvironmentEntry + nil, // 35: agent.ImportContainer.EnvironmentEntry + nil, // 36: agent.LogConfig.OptionsEntry + nil, // 37: agent.Marker.DeploymentEntry + nil, // 38: agent.Marker.ServiceEntry + nil, // 39: agent.Marker.IngressEntry + nil, // 40: agent.DagentContainerConfig.LabelsEntry + nil, // 41: agent.CraneContainerConfig.ExtraLBAnnotationsEntry + nil, // 42: agent.CommonContainerConfig.EnvironmentEntry + nil, // 43: agent.CommonContainerConfig.SecretsEntry + (*common.ContainerCommandRequest)(nil), // 44: common.ContainerCommandRequest + (*common.DeleteContainersRequest)(nil), // 45: common.DeleteContainersRequest + (*common.ContainerOrPrefix)(nil), // 46: common.ContainerOrPrefix + (common.VolumeType)(0), // 47: common.VolumeType + (common.DriverType)(0), // 48: common.DriverType + (common.ContainerState)(0), // 49: common.ContainerState + (common.RestartPolicy)(0), // 50: common.RestartPolicy + (common.NetworkMode)(0), // 51: common.NetworkMode + (common.DeploymentStrategy)(0), // 52: common.DeploymentStrategy + (*common.HealthCheckConfig)(nil), // 53: common.HealthCheckConfig + (*common.ResourceConfig)(nil), // 54: common.ResourceConfig + (common.ExposeStrategy)(0), // 55: common.ExposeStrategy + (*common.Routing)(nil), // 56: common.Routing + (*common.ConfigContainer)(nil), // 57: common.ConfigContainer + (*common.ContainerIdentifier)(nil), // 58: common.ContainerIdentifier (*common.Empty)(nil), // 59: common.Empty (*common.DeploymentStatusMessage)(nil), // 60: common.DeploymentStatusMessage (*common.ContainerStateListMessage)(nil), // 61: common.ContainerStateListMessage @@ -3141,94 +3036,93 @@ var file_protobuf_proto_agent_proto_goTypes = []interface{}{ (*common.ContainerInspectResponse)(nil), // 65: common.ContainerInspectResponse } var file_protobuf_proto_agent_proto_depIdxs = []int32{ - 6, // 0: agent.AgentCommand.deploy:type_name -> agent.VersionDeployRequest - 25, // 1: agent.AgentCommand.containerState:type_name -> agent.ContainerStateRequest - 26, // 2: agent.AgentCommand.containerDelete:type_name -> agent.ContainerDeleteRequest - 27, // 3: agent.AgentCommand.deployLegacy:type_name -> agent.DeployRequestLegacy + 6, // 0: agent.AgentCommand.deploy:type_name -> agent.DeployRequest + 24, // 1: agent.AgentCommand.containerState:type_name -> agent.ContainerStateRequest + 25, // 2: agent.AgentCommand.containerDelete:type_name -> agent.ContainerDeleteRequest + 26, // 3: agent.AgentCommand.deployLegacy:type_name -> agent.DeployRequestLegacy 7, // 4: agent.AgentCommand.listSecrets:type_name -> agent.ListSecretsRequest - 28, // 5: agent.AgentCommand.update:type_name -> agent.AgentUpdateRequest - 33, // 6: agent.AgentCommand.close:type_name -> agent.CloseConnectionRequest - 45, // 7: agent.AgentCommand.containerCommand:type_name -> common.ContainerCommandRequest - 46, // 8: agent.AgentCommand.deleteContainers:type_name -> common.DeleteContainersRequest - 31, // 9: agent.AgentCommand.containerLog:type_name -> agent.ContainerLogRequest - 29, // 10: agent.AgentCommand.replaceToken:type_name -> agent.ReplaceTokenRequest - 32, // 11: agent.AgentCommand.containerInspect:type_name -> agent.ContainerInspectRequest + 27, // 5: agent.AgentCommand.update:type_name -> agent.AgentUpdateRequest + 32, // 6: agent.AgentCommand.close:type_name -> agent.CloseConnectionRequest + 44, // 7: agent.AgentCommand.containerCommand:type_name -> common.ContainerCommandRequest + 45, // 8: agent.AgentCommand.deleteContainers:type_name -> common.DeleteContainersRequest + 30, // 9: agent.AgentCommand.containerLog:type_name -> agent.ContainerLogRequest + 28, // 10: agent.AgentCommand.replaceToken:type_name -> agent.ReplaceTokenRequest + 31, // 11: agent.AgentCommand.containerInspect:type_name -> agent.ContainerInspectRequest 3, // 12: agent.AgentCommandError.listSecrets:type_name -> agent.AgentError 3, // 13: agent.AgentCommandError.deleteContainers:type_name -> agent.AgentError 3, // 14: agent.AgentCommandError.containerLog:type_name -> agent.AgentError 3, // 15: agent.AgentCommandError.containerInspect:type_name -> agent.AgentError - 24, // 16: agent.VersionDeployRequest.requests:type_name -> agent.DeployRequest - 47, // 17: agent.ListSecretsRequest.container:type_name -> common.ContainerIdentifier - 34, // 18: agent.InstanceConfig.environment:type_name -> agent.InstanceConfig.EnvironmentEntry - 11, // 19: agent.PortRangeBinding.internal:type_name -> agent.PortRange - 11, // 20: agent.PortRangeBinding.external:type_name -> agent.PortRange - 48, // 21: agent.Volume.type:type_name -> common.VolumeType - 14, // 22: agent.InitContainer.volumes:type_name -> agent.VolumeLink - 35, // 23: agent.InitContainer.environment:type_name -> agent.InitContainer.EnvironmentEntry - 36, // 24: agent.ImportContainer.environment:type_name -> agent.ImportContainer.EnvironmentEntry - 49, // 25: agent.LogConfig.driver:type_name -> common.DriverType - 37, // 26: agent.LogConfig.options:type_name -> agent.LogConfig.OptionsEntry - 38, // 27: agent.Marker.deployment:type_name -> agent.Marker.DeploymentEntry - 39, // 28: agent.Marker.service:type_name -> agent.Marker.ServiceEntry - 40, // 29: agent.Marker.ingress:type_name -> agent.Marker.IngressEntry - 50, // 30: agent.ExpectedState.state:type_name -> common.ContainerState - 17, // 31: agent.DagentContainerConfig.logConfig:type_name -> agent.LogConfig - 51, // 32: agent.DagentContainerConfig.restartPolicy:type_name -> common.RestartPolicy - 52, // 33: agent.DagentContainerConfig.networkMode:type_name -> common.NetworkMode - 20, // 34: agent.DagentContainerConfig.expectedState:type_name -> agent.ExpectedState - 41, // 35: agent.DagentContainerConfig.labels:type_name -> agent.DagentContainerConfig.LabelsEntry - 53, // 36: agent.CraneContainerConfig.deploymentStrategy:type_name -> common.DeploymentStrategy - 54, // 37: agent.CraneContainerConfig.healthCheckConfig:type_name -> common.HealthCheckConfig - 55, // 38: agent.CraneContainerConfig.resourceConfig:type_name -> common.ResourceConfig - 18, // 39: agent.CraneContainerConfig.annotations:type_name -> agent.Marker - 18, // 40: agent.CraneContainerConfig.labels:type_name -> agent.Marker - 19, // 41: agent.CraneContainerConfig.metrics:type_name -> agent.Metrics - 42, // 42: agent.CraneContainerConfig.extraLBAnnotations:type_name -> agent.CraneContainerConfig.ExtraLBAnnotationsEntry - 56, // 43: agent.CommonContainerConfig.expose:type_name -> common.ExposeStrategy - 57, // 44: agent.CommonContainerConfig.routing:type_name -> common.Routing - 58, // 45: agent.CommonContainerConfig.configContainer:type_name -> common.ConfigContainer - 16, // 46: agent.CommonContainerConfig.importContainer:type_name -> agent.ImportContainer - 10, // 47: agent.CommonContainerConfig.ports:type_name -> agent.Port - 12, // 48: agent.CommonContainerConfig.portRanges:type_name -> agent.PortRangeBinding - 13, // 49: agent.CommonContainerConfig.volumes:type_name -> agent.Volume - 43, // 50: agent.CommonContainerConfig.environment:type_name -> agent.CommonContainerConfig.EnvironmentEntry - 44, // 51: agent.CommonContainerConfig.secrets:type_name -> agent.CommonContainerConfig.SecretsEntry - 15, // 52: agent.CommonContainerConfig.initContainers:type_name -> agent.InitContainer - 8, // 53: agent.DeployRequest.instanceConfig:type_name -> agent.InstanceConfig - 23, // 54: agent.DeployRequest.common:type_name -> agent.CommonContainerConfig - 21, // 55: agent.DeployRequest.dagent:type_name -> agent.DagentContainerConfig - 22, // 56: agent.DeployRequest.crane:type_name -> agent.CraneContainerConfig - 9, // 57: agent.DeployRequest.registryAuth:type_name -> agent.RegistryAuth - 47, // 58: agent.ContainerLogRequest.container:type_name -> common.ContainerIdentifier - 47, // 59: agent.ContainerInspectRequest.container:type_name -> common.ContainerIdentifier - 0, // 60: agent.CloseConnectionRequest.reason:type_name -> agent.CloseReason - 1, // 61: agent.Agent.Connect:input_type -> agent.AgentInfo - 4, // 62: agent.Agent.CommandError:input_type -> agent.AgentCommandError - 30, // 63: agent.Agent.AbortUpdate:input_type -> agent.AgentAbortUpdate - 59, // 64: agent.Agent.TokenReplaced:input_type -> common.Empty - 60, // 65: agent.Agent.DeploymentStatus:input_type -> common.DeploymentStatusMessage - 61, // 66: agent.Agent.ContainerState:input_type -> common.ContainerStateListMessage - 62, // 67: agent.Agent.ContainerLogStream:input_type -> common.ContainerLogMessage - 63, // 68: agent.Agent.SecretList:input_type -> common.ListSecretsResponse - 59, // 69: agent.Agent.DeleteContainers:input_type -> common.Empty - 64, // 70: agent.Agent.ContainerLog:input_type -> common.ContainerLogListResponse - 65, // 71: agent.Agent.ContainerInspect:input_type -> common.ContainerInspectResponse - 2, // 72: agent.Agent.Connect:output_type -> agent.AgentCommand - 59, // 73: agent.Agent.CommandError:output_type -> common.Empty - 59, // 74: agent.Agent.AbortUpdate:output_type -> common.Empty - 59, // 75: agent.Agent.TokenReplaced:output_type -> common.Empty - 59, // 76: agent.Agent.DeploymentStatus:output_type -> common.Empty - 59, // 77: agent.Agent.ContainerState:output_type -> common.Empty - 59, // 78: agent.Agent.ContainerLogStream:output_type -> common.Empty - 59, // 79: agent.Agent.SecretList:output_type -> common.Empty - 59, // 80: agent.Agent.DeleteContainers:output_type -> common.Empty - 59, // 81: agent.Agent.ContainerLog:output_type -> common.Empty - 59, // 82: agent.Agent.ContainerInspect:output_type -> common.Empty - 72, // [72:83] is the sub-list for method output_type - 61, // [61:72] is the sub-list for method input_type - 61, // [61:61] is the sub-list for extension type_name - 61, // [61:61] is the sub-list for extension extendee - 0, // [0:61] is the sub-list for field type_name + 33, // 16: agent.DeployRequest.secrets:type_name -> agent.DeployRequest.SecretsEntry + 23, // 17: agent.DeployRequest.requests:type_name -> agent.DeployWorkloadRequest + 46, // 18: agent.ListSecretsRequest.target:type_name -> common.ContainerOrPrefix + 10, // 19: agent.PortRangeBinding.internal:type_name -> agent.PortRange + 10, // 20: agent.PortRangeBinding.external:type_name -> agent.PortRange + 47, // 21: agent.Volume.type:type_name -> common.VolumeType + 13, // 22: agent.InitContainer.volumes:type_name -> agent.VolumeLink + 34, // 23: agent.InitContainer.environment:type_name -> agent.InitContainer.EnvironmentEntry + 35, // 24: agent.ImportContainer.environment:type_name -> agent.ImportContainer.EnvironmentEntry + 48, // 25: agent.LogConfig.driver:type_name -> common.DriverType + 36, // 26: agent.LogConfig.options:type_name -> agent.LogConfig.OptionsEntry + 37, // 27: agent.Marker.deployment:type_name -> agent.Marker.DeploymentEntry + 38, // 28: agent.Marker.service:type_name -> agent.Marker.ServiceEntry + 39, // 29: agent.Marker.ingress:type_name -> agent.Marker.IngressEntry + 49, // 30: agent.ExpectedState.state:type_name -> common.ContainerState + 16, // 31: agent.DagentContainerConfig.logConfig:type_name -> agent.LogConfig + 50, // 32: agent.DagentContainerConfig.restartPolicy:type_name -> common.RestartPolicy + 51, // 33: agent.DagentContainerConfig.networkMode:type_name -> common.NetworkMode + 19, // 34: agent.DagentContainerConfig.expectedState:type_name -> agent.ExpectedState + 40, // 35: agent.DagentContainerConfig.labels:type_name -> agent.DagentContainerConfig.LabelsEntry + 52, // 36: agent.CraneContainerConfig.deploymentStrategy:type_name -> common.DeploymentStrategy + 53, // 37: agent.CraneContainerConfig.healthCheckConfig:type_name -> common.HealthCheckConfig + 54, // 38: agent.CraneContainerConfig.resourceConfig:type_name -> common.ResourceConfig + 17, // 39: agent.CraneContainerConfig.annotations:type_name -> agent.Marker + 17, // 40: agent.CraneContainerConfig.labels:type_name -> agent.Marker + 18, // 41: agent.CraneContainerConfig.metrics:type_name -> agent.Metrics + 41, // 42: agent.CraneContainerConfig.extraLBAnnotations:type_name -> agent.CraneContainerConfig.ExtraLBAnnotationsEntry + 55, // 43: agent.CommonContainerConfig.expose:type_name -> common.ExposeStrategy + 56, // 44: agent.CommonContainerConfig.routing:type_name -> common.Routing + 57, // 45: agent.CommonContainerConfig.configContainer:type_name -> common.ConfigContainer + 15, // 46: agent.CommonContainerConfig.importContainer:type_name -> agent.ImportContainer + 9, // 47: agent.CommonContainerConfig.ports:type_name -> agent.Port + 11, // 48: agent.CommonContainerConfig.portRanges:type_name -> agent.PortRangeBinding + 12, // 49: agent.CommonContainerConfig.volumes:type_name -> agent.Volume + 42, // 50: agent.CommonContainerConfig.environment:type_name -> agent.CommonContainerConfig.EnvironmentEntry + 43, // 51: agent.CommonContainerConfig.secrets:type_name -> agent.CommonContainerConfig.SecretsEntry + 14, // 52: agent.CommonContainerConfig.initContainers:type_name -> agent.InitContainer + 22, // 53: agent.DeployWorkloadRequest.common:type_name -> agent.CommonContainerConfig + 20, // 54: agent.DeployWorkloadRequest.dagent:type_name -> agent.DagentContainerConfig + 21, // 55: agent.DeployWorkloadRequest.crane:type_name -> agent.CraneContainerConfig + 8, // 56: agent.DeployWorkloadRequest.registryAuth:type_name -> agent.RegistryAuth + 58, // 57: agent.ContainerLogRequest.container:type_name -> common.ContainerIdentifier + 58, // 58: agent.ContainerInspectRequest.container:type_name -> common.ContainerIdentifier + 0, // 59: agent.CloseConnectionRequest.reason:type_name -> agent.CloseReason + 1, // 60: agent.Agent.Connect:input_type -> agent.AgentInfo + 4, // 61: agent.Agent.CommandError:input_type -> agent.AgentCommandError + 29, // 62: agent.Agent.AbortUpdate:input_type -> agent.AgentAbortUpdate + 59, // 63: agent.Agent.TokenReplaced:input_type -> common.Empty + 60, // 64: agent.Agent.DeploymentStatus:input_type -> common.DeploymentStatusMessage + 61, // 65: agent.Agent.ContainerState:input_type -> common.ContainerStateListMessage + 62, // 66: agent.Agent.ContainerLogStream:input_type -> common.ContainerLogMessage + 63, // 67: agent.Agent.SecretList:input_type -> common.ListSecretsResponse + 59, // 68: agent.Agent.DeleteContainers:input_type -> common.Empty + 64, // 69: agent.Agent.ContainerLog:input_type -> common.ContainerLogListResponse + 65, // 70: agent.Agent.ContainerInspect:input_type -> common.ContainerInspectResponse + 2, // 71: agent.Agent.Connect:output_type -> agent.AgentCommand + 59, // 72: agent.Agent.CommandError:output_type -> common.Empty + 59, // 73: agent.Agent.AbortUpdate:output_type -> common.Empty + 59, // 74: agent.Agent.TokenReplaced:output_type -> common.Empty + 59, // 75: agent.Agent.DeploymentStatus:output_type -> common.Empty + 59, // 76: agent.Agent.ContainerState:output_type -> common.Empty + 59, // 77: agent.Agent.ContainerLogStream:output_type -> common.Empty + 59, // 78: agent.Agent.SecretList:output_type -> common.Empty + 59, // 79: agent.Agent.DeleteContainers:output_type -> common.Empty + 59, // 80: agent.Agent.ContainerLog:output_type -> common.Empty + 59, // 81: agent.Agent.ContainerInspect:output_type -> common.Empty + 71, // [71:82] is the sub-list for method output_type + 60, // [60:71] is the sub-list for method input_type + 60, // [60:60] is the sub-list for extension type_name + 60, // [60:60] is the sub-list for extension extendee + 0, // [0:60] is the sub-list for field type_name } func init() { file_protobuf_proto_agent_proto_init() } @@ -3298,7 +3192,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VersionDeployRequest); i { + switch v := v.(*DeployRequest); i { case 0: return &v.state case 1: @@ -3322,7 +3216,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*InstanceConfig); i { + switch v := v.(*RegistryAuth); i { case 0: return &v.state case 1: @@ -3334,7 +3228,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RegistryAuth); i { + switch v := v.(*Port); i { case 0: return &v.state case 1: @@ -3346,7 +3240,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Port); i { + switch v := v.(*PortRange); i { case 0: return &v.state case 1: @@ -3358,7 +3252,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortRange); i { + switch v := v.(*PortRangeBinding); i { case 0: return &v.state case 1: @@ -3370,7 +3264,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortRangeBinding); i { + switch v := v.(*Volume); i { case 0: return &v.state case 1: @@ -3382,7 +3276,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Volume); i { + switch v := v.(*VolumeLink); i { case 0: return &v.state case 1: @@ -3394,7 +3288,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VolumeLink); i { + switch v := v.(*InitContainer); i { case 0: return &v.state case 1: @@ -3406,7 +3300,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*InitContainer); i { + switch v := v.(*ImportContainer); i { case 0: return &v.state case 1: @@ -3418,7 +3312,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImportContainer); i { + switch v := v.(*LogConfig); i { case 0: return &v.state case 1: @@ -3430,7 +3324,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LogConfig); i { + switch v := v.(*Marker); i { case 0: return &v.state case 1: @@ -3442,7 +3336,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Marker); i { + switch v := v.(*Metrics); i { case 0: return &v.state case 1: @@ -3454,7 +3348,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Metrics); i { + switch v := v.(*ExpectedState); i { case 0: return &v.state case 1: @@ -3466,7 +3360,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExpectedState); i { + switch v := v.(*DagentContainerConfig); i { case 0: return &v.state case 1: @@ -3478,7 +3372,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DagentContainerConfig); i { + switch v := v.(*CraneContainerConfig); i { case 0: return &v.state case 1: @@ -3490,7 +3384,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CraneContainerConfig); i { + switch v := v.(*CommonContainerConfig); i { case 0: return &v.state case 1: @@ -3502,7 +3396,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CommonContainerConfig); i { + switch v := v.(*DeployWorkloadRequest); i { case 0: return &v.state case 1: @@ -3514,18 +3408,6 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeployRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_protobuf_proto_agent_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ContainerStateRequest); i { case 0: return &v.state @@ -3537,7 +3419,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ContainerDeleteRequest); i { case 0: return &v.state @@ -3549,7 +3431,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeployRequestLegacy); i { case 0: return &v.state @@ -3561,7 +3443,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AgentUpdateRequest); i { case 0: return &v.state @@ -3573,7 +3455,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ReplaceTokenRequest); i { case 0: return &v.state @@ -3585,7 +3467,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AgentAbortUpdate); i { case 0: return &v.state @@ -3597,7 +3479,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ContainerLogRequest); i { case 0: return &v.state @@ -3609,7 +3491,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ContainerInspectRequest); i { case 0: return &v.state @@ -3621,7 +3503,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CloseConnectionRequest); i { case 0: return &v.state @@ -3655,23 +3537,22 @@ func file_protobuf_proto_agent_proto_init() { (*AgentCommandError_ContainerLog)(nil), (*AgentCommandError_ContainerInspect)(nil), } - file_protobuf_proto_agent_proto_msgTypes[7].OneofWrappers = []interface{}{} - file_protobuf_proto_agent_proto_msgTypes[9].OneofWrappers = []interface{}{} - file_protobuf_proto_agent_proto_msgTypes[12].OneofWrappers = []interface{}{} - file_protobuf_proto_agent_proto_msgTypes[14].OneofWrappers = []interface{}{} + file_protobuf_proto_agent_proto_msgTypes[8].OneofWrappers = []interface{}{} + file_protobuf_proto_agent_proto_msgTypes[11].OneofWrappers = []interface{}{} + file_protobuf_proto_agent_proto_msgTypes[13].OneofWrappers = []interface{}{} + file_protobuf_proto_agent_proto_msgTypes[18].OneofWrappers = []interface{}{} file_protobuf_proto_agent_proto_msgTypes[19].OneofWrappers = []interface{}{} file_protobuf_proto_agent_proto_msgTypes[20].OneofWrappers = []interface{}{} file_protobuf_proto_agent_proto_msgTypes[21].OneofWrappers = []interface{}{} file_protobuf_proto_agent_proto_msgTypes[22].OneofWrappers = []interface{}{} file_protobuf_proto_agent_proto_msgTypes[23].OneofWrappers = []interface{}{} - file_protobuf_proto_agent_proto_msgTypes[24].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_protobuf_proto_agent_proto_rawDesc, NumEnums: 1, - NumMessages: 44, + NumMessages: 43, NumExtensions: 0, NumServices: 1, }, diff --git a/protobuf/go/common/common.pb.go b/protobuf/go/common/common.pb.go index bab113cc9c..c03b607c53 100644 --- a/protobuf/go/common/common.pb.go +++ b/protobuf/go/common/common.pb.go @@ -1632,20 +1632,20 @@ func (x *KeyValue) GetValue() string { return "" } -type ListSecretsResponse struct { +type ContainerOrPrefix struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - PublicKey string `protobuf:"bytes,3,opt,name=publicKey,proto3" json:"publicKey,omitempty"` - HasKeys bool `protobuf:"varint,4,opt,name=hasKeys,proto3" json:"hasKeys,omitempty"` - Keys []string `protobuf:"bytes,5,rep,name=keys,proto3" json:"keys,omitempty"` + // Types that are assignable to Target: + // + // *ContainerOrPrefix_Container + // *ContainerOrPrefix_Prefix + Target isContainerOrPrefix_Target `protobuf_oneof:"target"` } -func (x *ListSecretsResponse) Reset() { - *x = ListSecretsResponse{} +func (x *ContainerOrPrefix) Reset() { + *x = ContainerOrPrefix{} if protoimpl.UnsafeEnabled { mi := &file_protobuf_proto_common_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1653,13 +1653,13 @@ func (x *ListSecretsResponse) Reset() { } } -func (x *ListSecretsResponse) String() string { +func (x *ContainerOrPrefix) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListSecretsResponse) ProtoMessage() {} +func (*ContainerOrPrefix) ProtoMessage() {} -func (x *ListSecretsResponse) ProtoReflect() protoreflect.Message { +func (x *ContainerOrPrefix) ProtoReflect() protoreflect.Message { mi := &file_protobuf_proto_common_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1671,37 +1671,102 @@ func (x *ListSecretsResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListSecretsResponse.ProtoReflect.Descriptor instead. -func (*ListSecretsResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use ContainerOrPrefix.ProtoReflect.Descriptor instead. +func (*ContainerOrPrefix) Descriptor() ([]byte, []int) { return file_protobuf_proto_common_proto_rawDescGZIP(), []int{16} } -func (x *ListSecretsResponse) GetPrefix() string { - if x != nil { +func (m *ContainerOrPrefix) GetTarget() isContainerOrPrefix_Target { + if m != nil { + return m.Target + } + return nil +} + +func (x *ContainerOrPrefix) GetContainer() *ContainerIdentifier { + if x, ok := x.GetTarget().(*ContainerOrPrefix_Container); ok { + return x.Container + } + return nil +} + +func (x *ContainerOrPrefix) GetPrefix() string { + if x, ok := x.GetTarget().(*ContainerOrPrefix_Prefix); ok { return x.Prefix } return "" } -func (x *ListSecretsResponse) GetName() string { - if x != nil { - return x.Name +type isContainerOrPrefix_Target interface { + isContainerOrPrefix_Target() +} + +type ContainerOrPrefix_Container struct { + Container *ContainerIdentifier `protobuf:"bytes,1,opt,name=container,proto3,oneof"` +} + +type ContainerOrPrefix_Prefix struct { + Prefix string `protobuf:"bytes,2,opt,name=prefix,proto3,oneof"` +} + +func (*ContainerOrPrefix_Container) isContainerOrPrefix_Target() {} + +func (*ContainerOrPrefix_Prefix) isContainerOrPrefix_Target() {} + +type ListSecretsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Target *ContainerOrPrefix `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` + PublicKey string `protobuf:"bytes,3,opt,name=publicKey,proto3" json:"publicKey,omitempty"` + Keys []string `protobuf:"bytes,4,rep,name=keys,proto3" json:"keys,omitempty"` +} + +func (x *ListSecretsResponse) Reset() { + *x = ListSecretsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_protobuf_proto_common_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } - return "" } -func (x *ListSecretsResponse) GetPublicKey() string { +func (x *ListSecretsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSecretsResponse) ProtoMessage() {} + +func (x *ListSecretsResponse) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_proto_common_proto_msgTypes[17] + 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 ListSecretsResponse.ProtoReflect.Descriptor instead. +func (*ListSecretsResponse) Descriptor() ([]byte, []int) { + return file_protobuf_proto_common_proto_rawDescGZIP(), []int{17} +} + +func (x *ListSecretsResponse) GetTarget() *ContainerOrPrefix { if x != nil { - return x.PublicKey + return x.Target } - return "" + return nil } -func (x *ListSecretsResponse) GetHasKeys() bool { +func (x *ListSecretsResponse) GetPublicKey() string { if x != nil { - return x.HasKeys + return x.PublicKey } - return false + return "" } func (x *ListSecretsResponse) GetKeys() []string { @@ -1723,7 +1788,7 @@ type UniqueKey struct { func (x *UniqueKey) Reset() { *x = UniqueKey{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_common_proto_msgTypes[17] + mi := &file_protobuf_proto_common_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1736,7 +1801,7 @@ func (x *UniqueKey) String() string { func (*UniqueKey) ProtoMessage() {} func (x *UniqueKey) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_common_proto_msgTypes[17] + mi := &file_protobuf_proto_common_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1749,7 +1814,7 @@ func (x *UniqueKey) ProtoReflect() protoreflect.Message { // Deprecated: Use UniqueKey.ProtoReflect.Descriptor instead. func (*UniqueKey) Descriptor() ([]byte, []int) { - return file_protobuf_proto_common_proto_rawDescGZIP(), []int{17} + return file_protobuf_proto_common_proto_rawDescGZIP(), []int{18} } func (x *UniqueKey) GetId() string { @@ -1778,7 +1843,7 @@ type ContainerIdentifier struct { func (x *ContainerIdentifier) Reset() { *x = ContainerIdentifier{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_common_proto_msgTypes[18] + mi := &file_protobuf_proto_common_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1791,7 +1856,7 @@ func (x *ContainerIdentifier) String() string { func (*ContainerIdentifier) ProtoMessage() {} func (x *ContainerIdentifier) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_common_proto_msgTypes[18] + mi := &file_protobuf_proto_common_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1804,7 +1869,7 @@ func (x *ContainerIdentifier) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerIdentifier.ProtoReflect.Descriptor instead. func (*ContainerIdentifier) Descriptor() ([]byte, []int) { - return file_protobuf_proto_common_proto_rawDescGZIP(), []int{18} + return file_protobuf_proto_common_proto_rawDescGZIP(), []int{19} } func (x *ContainerIdentifier) GetPrefix() string { @@ -1833,7 +1898,7 @@ type ContainerCommandRequest struct { func (x *ContainerCommandRequest) Reset() { *x = ContainerCommandRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_common_proto_msgTypes[19] + mi := &file_protobuf_proto_common_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1846,7 +1911,7 @@ func (x *ContainerCommandRequest) String() string { func (*ContainerCommandRequest) ProtoMessage() {} func (x *ContainerCommandRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_common_proto_msgTypes[19] + mi := &file_protobuf_proto_common_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1859,7 +1924,7 @@ func (x *ContainerCommandRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerCommandRequest.ProtoReflect.Descriptor instead. func (*ContainerCommandRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_common_proto_rawDescGZIP(), []int{19} + return file_protobuf_proto_common_proto_rawDescGZIP(), []int{20} } func (x *ContainerCommandRequest) GetContainer() *ContainerIdentifier { @@ -1881,17 +1946,13 @@ type DeleteContainersRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Types that are assignable to Target: - // - // *DeleteContainersRequest_Container - // *DeleteContainersRequest_Prefix - Target isDeleteContainersRequest_Target `protobuf_oneof:"target"` + Target *ContainerOrPrefix `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` } func (x *DeleteContainersRequest) Reset() { *x = DeleteContainersRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_common_proto_msgTypes[20] + mi := &file_protobuf_proto_common_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1904,7 +1965,7 @@ func (x *DeleteContainersRequest) String() string { func (*DeleteContainersRequest) ProtoMessage() {} func (x *DeleteContainersRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_common_proto_msgTypes[20] + mi := &file_protobuf_proto_common_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1917,46 +1978,16 @@ func (x *DeleteContainersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteContainersRequest.ProtoReflect.Descriptor instead. func (*DeleteContainersRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_common_proto_rawDescGZIP(), []int{20} + return file_protobuf_proto_common_proto_rawDescGZIP(), []int{21} } -func (m *DeleteContainersRequest) GetTarget() isDeleteContainersRequest_Target { - if m != nil { - return m.Target - } - return nil -} - -func (x *DeleteContainersRequest) GetContainer() *ContainerIdentifier { - if x, ok := x.GetTarget().(*DeleteContainersRequest_Container); ok { - return x.Container +func (x *DeleteContainersRequest) GetTarget() *ContainerOrPrefix { + if x != nil { + return x.Target } return nil } -func (x *DeleteContainersRequest) GetPrefix() string { - if x, ok := x.GetTarget().(*DeleteContainersRequest_Prefix); ok { - return x.Prefix - } - return "" -} - -type isDeleteContainersRequest_Target interface { - isDeleteContainersRequest_Target() -} - -type DeleteContainersRequest_Container struct { - Container *ContainerIdentifier `protobuf:"bytes,201,opt,name=container,proto3,oneof"` -} - -type DeleteContainersRequest_Prefix struct { - Prefix string `protobuf:"bytes,202,opt,name=prefix,proto3,oneof"` -} - -func (*DeleteContainersRequest_Container) isDeleteContainersRequest_Target() {} - -func (*DeleteContainersRequest_Prefix) isDeleteContainersRequest_Target() {} - var File_protobuf_proto_common_proto protoreflect.FileDescriptor var file_protobuf_proto_common_proto_rawDesc = []byte{ @@ -2106,117 +2137,120 @@ var file_protobuf_proto_common_proto_rawDesc = []byte{ 0x75, 0x65, 0x73, 0x74, 0x73, 0x22, 0x32, 0x0a, 0x08, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x65, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x8d, 0x01, 0x0a, 0x13, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x74, 0x0a, 0x11, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x72, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x3b, + 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, + 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x06, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x42, 0x08, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, + 0x7a, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x72, 0x50, 0x72, 0x65, 0x66, 0x69, + 0x78, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x2d, 0x0a, 0x09, 0x55, + 0x6e, 0x69, 0x71, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x64, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x41, 0x0a, 0x13, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, - 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x68, - 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, - 0x73, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x2d, 0x0a, 0x09, 0x55, 0x6e, 0x69, - 0x71, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x64, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x65, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x41, 0x0a, 0x13, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x8e, 0x01, 0x0a, 0x17, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x7c, 0x0a, 0x17, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x18, 0xc9, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, - 0xca, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x42, 0x08, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2a, 0x64, 0x0a, 0x0e, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x0a, 0x1b, - 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, - 0x07, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, - 0x49, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x58, 0x49, 0x54, 0x45, - 0x44, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x44, 0x10, 0x04, - 0x2a, 0x8f, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x1d, 0x44, 0x45, 0x50, 0x4c, 0x4f, 0x59, 0x4d, - 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, - 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, 0x45, 0x50, - 0x41, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x49, 0x4e, 0x5f, 0x50, 0x52, - 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x55, 0x43, 0x43, - 0x45, 0x53, 0x53, 0x46, 0x55, 0x4c, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, - 0x45, 0x44, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x4f, 0x42, 0x53, 0x4f, 0x4c, 0x45, 0x54, 0x45, - 0x10, 0x05, 0x12, 0x0e, 0x0a, 0x0a, 0x44, 0x4f, 0x57, 0x4e, 0x47, 0x52, 0x41, 0x44, 0x45, 0x44, - 0x10, 0x06, 0x2a, 0x5e, 0x0a, 0x16, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x22, 0x0a, 0x1e, - 0x44, 0x45, 0x50, 0x4c, 0x4f, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, - 0x47, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, - 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x10, 0x03, 0x2a, 0x4b, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x64, - 0x65, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x4d, 0x4f, 0x44, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x8e, 0x01, + 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x4c, + 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x72, 0x50, 0x72, + 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2a, 0x64, 0x0a, 0x0e, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1f, + 0x0a, 0x1b, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, - 0x0a, 0x0a, 0x06, 0x42, 0x52, 0x49, 0x44, 0x47, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, - 0x4f, 0x53, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x06, 0x2a, - 0x6e, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x12, 0x16, 0x0a, 0x12, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, - 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x44, 0x45, - 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x01, 0x12, 0x06, 0x0a, 0x02, 0x4e, 0x4f, 0x10, 0x02, 0x12, - 0x0e, 0x0a, 0x0a, 0x4f, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x03, 0x12, - 0x0a, 0x0a, 0x06, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, 0x55, - 0x4e, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x05, 0x2a, - 0x5b, 0x0a, 0x12, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, - 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x50, 0x4c, 0x4f, 0x59, 0x4d, - 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, - 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x4f, 0x4c, 0x4c, - 0x49, 0x4e, 0x47, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x2a, 0x61, 0x0a, 0x0a, - 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x56, 0x4f, - 0x4c, 0x55, 0x4d, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x52, 0x4f, 0x10, 0x01, 0x12, - 0x07, 0x0a, 0x03, 0x52, 0x57, 0x4f, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x52, 0x57, 0x58, 0x10, - 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x45, 0x4d, 0x10, 0x04, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x4d, - 0x50, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x45, 0x43, 0x52, 0x45, 0x54, 0x10, 0x06, 0x2a, - 0xdf, 0x01, 0x0a, 0x0a, 0x44, 0x72, 0x69, 0x76, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, - 0x0a, 0x17, 0x44, 0x52, 0x49, 0x56, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x4e, - 0x4f, 0x44, 0x45, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x01, 0x12, 0x14, 0x0a, - 0x10, 0x44, 0x52, 0x49, 0x56, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x4f, 0x4e, - 0x45, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x47, 0x43, 0x50, 0x4c, 0x4f, 0x47, 0x53, 0x10, 0x03, - 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x4a, - 0x53, 0x4f, 0x4e, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, - 0x53, 0x4c, 0x4f, 0x47, 0x10, 0x06, 0x12, 0x0c, 0x0a, 0x08, 0x4a, 0x4f, 0x55, 0x52, 0x4e, 0x41, - 0x4c, 0x44, 0x10, 0x07, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x45, 0x4c, 0x46, 0x10, 0x08, 0x12, 0x0b, - 0x0a, 0x07, 0x46, 0x4c, 0x55, 0x45, 0x4e, 0x54, 0x44, 0x10, 0x09, 0x12, 0x0b, 0x0a, 0x07, 0x41, - 0x57, 0x53, 0x4c, 0x4f, 0x47, 0x53, 0x10, 0x0a, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x50, 0x4c, 0x55, - 0x4e, 0x4b, 0x10, 0x0b, 0x12, 0x0b, 0x0a, 0x07, 0x45, 0x54, 0x57, 0x4c, 0x4f, 0x47, 0x53, 0x10, - 0x0c, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, 0x4f, 0x47, 0x45, 0x4e, 0x54, 0x52, 0x49, 0x45, 0x53, 0x10, - 0x0d, 0x2a, 0x5f, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x67, 0x79, 0x12, 0x1f, 0x0a, 0x1b, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x53, 0x54, - 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x4f, 0x4e, 0x45, 0x5f, 0x45, 0x53, 0x10, - 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x10, 0x02, 0x12, 0x13, 0x0a, - 0x0f, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x54, 0x4c, 0x53, - 0x10, 0x03, 0x2a, 0x79, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x4f, 0x4e, 0x54, - 0x41, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, - 0x0f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, - 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x4f, 0x50, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, - 0x49, 0x4e, 0x45, 0x52, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, - 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x10, 0x03, 0x42, 0x36, 0x5a, - 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x79, 0x72, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x2d, 0x69, 0x6f, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x69, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, 0x2f, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x0b, 0x0a, 0x07, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, + 0x57, 0x41, 0x49, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x58, 0x49, + 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x44, + 0x10, 0x04, 0x2a, 0x8f, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x1d, 0x44, 0x45, 0x50, 0x4c, 0x4f, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, + 0x45, 0x50, 0x41, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x49, 0x4e, 0x5f, + 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x55, + 0x43, 0x43, 0x45, 0x53, 0x53, 0x46, 0x55, 0x4c, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, + 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x4f, 0x42, 0x53, 0x4f, 0x4c, 0x45, + 0x54, 0x45, 0x10, 0x05, 0x12, 0x0e, 0x0a, 0x0a, 0x44, 0x4f, 0x57, 0x4e, 0x47, 0x52, 0x41, 0x44, + 0x45, 0x44, 0x10, 0x06, 0x2a, 0x5e, 0x0a, 0x16, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x22, + 0x0a, 0x1e, 0x44, 0x45, 0x50, 0x4c, 0x4f, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x4d, 0x45, 0x53, + 0x53, 0x41, 0x47, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, + 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x03, 0x2a, 0x4b, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x4d, + 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x52, 0x49, 0x44, 0x47, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, + 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, + 0x06, 0x2a, 0x6e, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, + 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x01, 0x12, 0x06, 0x0a, 0x02, 0x4e, 0x4f, 0x10, + 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x4f, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, + 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x04, 0x12, 0x12, 0x0a, + 0x0e, 0x55, 0x4e, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, + 0x05, 0x2a, 0x5b, 0x0a, 0x12, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x50, 0x4c, 0x4f, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, + 0x52, 0x45, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x4f, + 0x4c, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x2a, 0x61, + 0x0a, 0x0a, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, + 0x56, 0x4f, 0x4c, 0x55, 0x4d, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, + 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x52, 0x4f, 0x10, + 0x01, 0x12, 0x07, 0x0a, 0x03, 0x52, 0x57, 0x4f, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x52, 0x57, + 0x58, 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x45, 0x4d, 0x10, 0x04, 0x12, 0x07, 0x0a, 0x03, + 0x54, 0x4d, 0x50, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x45, 0x43, 0x52, 0x45, 0x54, 0x10, + 0x06, 0x2a, 0xdf, 0x01, 0x0a, 0x0a, 0x44, 0x72, 0x69, 0x76, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x52, 0x49, 0x56, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, + 0x0c, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x01, 0x12, + 0x14, 0x0a, 0x10, 0x44, 0x52, 0x49, 0x56, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, + 0x4f, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x47, 0x43, 0x50, 0x4c, 0x4f, 0x47, 0x53, + 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x04, 0x12, 0x0d, 0x0a, + 0x09, 0x4a, 0x53, 0x4f, 0x4e, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, + 0x53, 0x59, 0x53, 0x4c, 0x4f, 0x47, 0x10, 0x06, 0x12, 0x0c, 0x0a, 0x08, 0x4a, 0x4f, 0x55, 0x52, + 0x4e, 0x41, 0x4c, 0x44, 0x10, 0x07, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x45, 0x4c, 0x46, 0x10, 0x08, + 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x4c, 0x55, 0x45, 0x4e, 0x54, 0x44, 0x10, 0x09, 0x12, 0x0b, 0x0a, + 0x07, 0x41, 0x57, 0x53, 0x4c, 0x4f, 0x47, 0x53, 0x10, 0x0a, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x50, + 0x4c, 0x55, 0x4e, 0x4b, 0x10, 0x0b, 0x12, 0x0b, 0x0a, 0x07, 0x45, 0x54, 0x57, 0x4c, 0x4f, 0x47, + 0x53, 0x10, 0x0c, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, 0x4f, 0x47, 0x45, 0x4e, 0x54, 0x52, 0x49, 0x45, + 0x53, 0x10, 0x0d, 0x2a, 0x5f, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x72, + 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x1f, 0x0a, 0x1b, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, + 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x4f, 0x4e, 0x45, 0x5f, 0x45, + 0x53, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x10, 0x02, 0x12, + 0x13, 0x0a, 0x0f, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x54, + 0x4c, 0x53, 0x10, 0x03, 0x2a, 0x79, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x4f, + 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, + 0x45, 0x52, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x4f, 0x50, 0x5f, 0x43, 0x4f, 0x4e, + 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x52, 0x45, 0x53, 0x54, + 0x41, 0x52, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x10, 0x03, 0x42, + 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x79, + 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2d, 0x69, 0x6f, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x69, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, + 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2232,7 +2266,7 @@ func file_protobuf_proto_common_proto_rawDescGZIP() []byte { } var file_protobuf_proto_common_proto_enumTypes = make([]protoimpl.EnumInfo, 10) -var file_protobuf_proto_common_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_protobuf_proto_common_proto_msgTypes = make([]protoimpl.MessageInfo, 23) var file_protobuf_proto_common_proto_goTypes = []interface{}{ (ContainerState)(0), // 0: common.ContainerState (DeploymentStatus)(0), // 1: common.DeploymentStatus @@ -2260,13 +2294,14 @@ var file_protobuf_proto_common_proto_goTypes = []interface{}{ (*Resource)(nil), // 23: common.Resource (*ResourceConfig)(nil), // 24: common.ResourceConfig (*KeyValue)(nil), // 25: common.KeyValue - (*ListSecretsResponse)(nil), // 26: common.ListSecretsResponse - (*UniqueKey)(nil), // 27: common.UniqueKey - (*ContainerIdentifier)(nil), // 28: common.ContainerIdentifier - (*ContainerCommandRequest)(nil), // 29: common.ContainerCommandRequest - (*DeleteContainersRequest)(nil), // 30: common.DeleteContainersRequest - nil, // 31: common.ContainerStateItem.LabelsEntry - (*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp + (*ContainerOrPrefix)(nil), // 26: common.ContainerOrPrefix + (*ListSecretsResponse)(nil), // 27: common.ListSecretsResponse + (*UniqueKey)(nil), // 28: common.UniqueKey + (*ContainerIdentifier)(nil), // 29: common.ContainerIdentifier + (*ContainerCommandRequest)(nil), // 30: common.ContainerCommandRequest + (*DeleteContainersRequest)(nil), // 31: common.DeleteContainersRequest + nil, // 32: common.ContainerStateItem.LabelsEntry + (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp } var file_protobuf_proto_common_proto_depIdxs = []int32{ 0, // 0: common.InstanceDeploymentItem.state:type_name -> common.ContainerState @@ -2275,21 +2310,23 @@ var file_protobuf_proto_common_proto_depIdxs = []int32{ 12, // 3: common.DeploymentStatusMessage.containerProgress:type_name -> common.DeployContainerProgress 2, // 4: common.DeploymentStatusMessage.logLevel:type_name -> common.DeploymentMessageLevel 16, // 5: common.ContainerStateListMessage.data:type_name -> common.ContainerStateItem - 28, // 6: common.ContainerStateItem.id:type_name -> common.ContainerIdentifier - 32, // 7: common.ContainerStateItem.createdAt:type_name -> google.protobuf.Timestamp + 29, // 6: common.ContainerStateItem.id:type_name -> common.ContainerIdentifier + 33, // 7: common.ContainerStateItem.createdAt:type_name -> google.protobuf.Timestamp 0, // 8: common.ContainerStateItem.state:type_name -> common.ContainerState 14, // 9: common.ContainerStateItem.ports:type_name -> common.ContainerStateItemPort - 31, // 10: common.ContainerStateItem.labels:type_name -> common.ContainerStateItem.LabelsEntry + 32, // 10: common.ContainerStateItem.labels:type_name -> common.ContainerStateItem.LabelsEntry 23, // 11: common.ResourceConfig.limits:type_name -> common.Resource 23, // 12: common.ResourceConfig.requests:type_name -> common.Resource - 28, // 13: common.ContainerCommandRequest.container:type_name -> common.ContainerIdentifier - 9, // 14: common.ContainerCommandRequest.operation:type_name -> common.ContainerOperation - 28, // 15: common.DeleteContainersRequest.container:type_name -> common.ContainerIdentifier - 16, // [16:16] is the sub-list for method output_type - 16, // [16:16] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 29, // 13: common.ContainerOrPrefix.container:type_name -> common.ContainerIdentifier + 26, // 14: common.ListSecretsResponse.target:type_name -> common.ContainerOrPrefix + 29, // 15: common.ContainerCommandRequest.container:type_name -> common.ContainerIdentifier + 9, // 16: common.ContainerCommandRequest.operation:type_name -> common.ContainerOperation + 26, // 17: common.DeleteContainersRequest.target:type_name -> common.ContainerOrPrefix + 18, // [18:18] is the sub-list for method output_type + 18, // [18:18] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name } func init() { file_protobuf_proto_common_proto_init() } @@ -2491,7 +2528,7 @@ func file_protobuf_proto_common_proto_init() { } } file_protobuf_proto_common_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListSecretsResponse); i { + switch v := v.(*ContainerOrPrefix); i { case 0: return &v.state case 1: @@ -2503,7 +2540,7 @@ func file_protobuf_proto_common_proto_init() { } } file_protobuf_proto_common_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UniqueKey); i { + switch v := v.(*ListSecretsResponse); i { case 0: return &v.state case 1: @@ -2515,7 +2552,7 @@ func file_protobuf_proto_common_proto_init() { } } file_protobuf_proto_common_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ContainerIdentifier); i { + switch v := v.(*UniqueKey); i { case 0: return &v.state case 1: @@ -2527,7 +2564,7 @@ func file_protobuf_proto_common_proto_init() { } } file_protobuf_proto_common_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ContainerCommandRequest); i { + switch v := v.(*ContainerIdentifier); i { case 0: return &v.state case 1: @@ -2539,6 +2576,18 @@ func file_protobuf_proto_common_proto_init() { } } file_protobuf_proto_common_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ContainerCommandRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_protobuf_proto_common_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteContainersRequest); i { case 0: return &v.state @@ -2561,9 +2610,9 @@ func file_protobuf_proto_common_proto_init() { file_protobuf_proto_common_proto_msgTypes[12].OneofWrappers = []interface{}{} file_protobuf_proto_common_proto_msgTypes[13].OneofWrappers = []interface{}{} file_protobuf_proto_common_proto_msgTypes[14].OneofWrappers = []interface{}{} - file_protobuf_proto_common_proto_msgTypes[20].OneofWrappers = []interface{}{ - (*DeleteContainersRequest_Container)(nil), - (*DeleteContainersRequest_Prefix)(nil), + file_protobuf_proto_common_proto_msgTypes[16].OneofWrappers = []interface{}{ + (*ContainerOrPrefix_Container)(nil), + (*ContainerOrPrefix_Prefix)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -2571,7 +2620,7 @@ func file_protobuf_proto_common_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_protobuf_proto_common_proto_rawDesc, NumEnums: 10, - NumMessages: 22, + NumMessages: 23, NumExtensions: 0, NumServices: 0, }, diff --git a/protobuf/proto/agent.proto b/protobuf/proto/agent.proto index 90ae346d5b..57d949f538 100644 --- a/protobuf/proto/agent.proto +++ b/protobuf/proto/agent.proto @@ -36,7 +36,6 @@ service Agent { rpc ContainerLogStream(stream common.ContainerLogMessage) returns (common.Empty); - // one-shot requests rpc SecretList(common.ListSecretsResponse) returns (common.Empty); rpc DeleteContainers(common.Empty) returns (common.Empty); @@ -57,7 +56,7 @@ message AgentInfo { message AgentCommand { oneof command { - VersionDeployRequest deploy = 1; + DeployRequest deploy = 1; ContainerStateRequest containerState = 2; ContainerDeleteRequest containerDelete = 3; DeployRequestLegacy deployLegacy = 4; @@ -92,31 +91,20 @@ message AgentCommandError { */ message DeployResponse { bool started = 1; } -message VersionDeployRequest { +message DeployRequest { string id = 1; string versionName = 2; string releaseNotes = 3; + string prefix = 4; - repeated DeployRequest requests = 4; + map secrets = 5; + repeated DeployWorkloadRequest requests = 6; } /* * Request for a keys of existing secrets in a prefix, eg. namespace */ -message ListSecretsRequest { common.ContainerIdentifier container = 1; } - -/** - * Deploys a single container - * - */ -message InstanceConfig { - /* - prefix mapped into host folder structure, - used as namespace id - */ - string prefix = 1; - optional string mountPath = 2; // mount path of instance (docker only) - map environment = 3; // environment variable map - optional string repositoryPrefix = 4; // registry repo prefix +message ListSecretsRequest { + common.ContainerOrPrefix target = 1; } message RegistryAuth { @@ -241,25 +229,19 @@ message CommonContainerConfig { repeated InitContainer initContainers = 1007; } -message DeployRequest { +message DeployWorkloadRequest { string id = 1; - string containerName = 2; - - /* InstanceConfig is set for multiple containers */ - InstanceConfig instanceConfig = 3; /* ContainerConfigs */ - optional CommonContainerConfig common = 4; - optional DagentContainerConfig dagent = 5; - optional CraneContainerConfig crane = 6; + optional CommonContainerConfig common = 2; + optional DagentContainerConfig dagent = 3; + optional CraneContainerConfig crane = 4; - /* Runtime info and requirements of a container */ - optional string runtimeConfig = 7; - optional string registry = 8; - string imageName = 9; - string tag = 10; + optional string registry = 5; + string imageName = 6; + string tag = 7; - optional RegistryAuth registryAuth = 11; + optional RegistryAuth registryAuth = 8; } message ContainerStateRequest { diff --git a/protobuf/proto/common.proto b/protobuf/proto/common.proto index 35c6114c61..98f5aa2c53 100644 --- a/protobuf/proto/common.proto +++ b/protobuf/proto/common.proto @@ -187,12 +187,17 @@ message KeyValue { string value = 101; } +message ContainerOrPrefix { + oneof target { + common.ContainerIdentifier container = 1; + string prefix = 2; + } +} + message ListSecretsResponse { - string prefix = 1; - string name = 2; + ContainerOrPrefix target = 1; string publicKey = 3; - bool hasKeys = 4; - repeated string keys = 5; + repeated string keys = 4; } message UniqueKey { @@ -218,8 +223,5 @@ message ContainerCommandRequest { } message DeleteContainersRequest { - oneof target { - common.ContainerIdentifier container = 201; - string prefix = 202; - } + ContainerOrPrefix target = 1; } diff --git a/web/crux-ui/e2e/utils/config-bundle.ts b/web/crux-ui/e2e/utils/config-bundle.ts index 203d482e23..99b4f98513 100644 --- a/web/crux-ui/e2e/utils/config-bundle.ts +++ b/web/crux-ui/e2e/utils/config-bundle.ts @@ -1,13 +1,13 @@ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/prefer-default-export */ -import { PatchConfigBundleMessage, WS_TYPE_PATCH_CONFIG_BUNDLE } from '@app/models' import { Page, expect } from '@playwright/test' import { TEAM_ROUTES } from './common' import { waitSocketRef, wsPatchSent } from './websocket' +import { PatchConfigMessage, WS_TYPE_PATCH_CONFIG } from '@app/models' -const matchPatchEnvironment = (expected: Record) => (message: PatchConfigBundleMessage) => +const matchPatchEnvironment = (expected: Record) => (message: PatchConfigMessage) => Object.entries(expected).every( - ([key, value]) => message.environment?.find(it => it.key === key && it.value === value), + ([key, value]) => message.config?.environment?.find(it => it.key === key && it.value === value), ) export const createConfigBundle = async (page: Page, name: string, data: Record): Promise => { @@ -18,19 +18,24 @@ export const createConfigBundle = async (page: Page, name: string, data: Record< await expect(page.locator('h4')).toContainText('New config bundle') await page.locator('input[name=name] >> visible=true').fill(name) - const sock = waitSocketRef(page) await page.locator('text=Save').click() await page.waitForURL(`${TEAM_ROUTES.configBundle.list()}/**`) - await page.waitForSelector(`h4:text-is("View ${name}")`) + await page.waitForSelector(`h3:text-is("${name}")`) const configBundleId = page.url().split('/').pop() + const sock = waitSocketRef(page) + await page.locator('button:has-text("Config")').click() + await page.waitForURL(TEAM_ROUTES.containerConfig.details('**')) + + const configId = page.url().split('/').pop() + const ws = await sock - const wsRoute = TEAM_ROUTES.configBundle.detailsSocket(configBundleId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(configId) - await page.locator('button:has-text("Edit")').click() + await page.locator('button:has-text("Environment")').click() - const wsPatchReceived = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG_BUNDLE, matchPatchEnvironment(data)) + const wsPatchReceived = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, matchPatchEnvironment(data)) const entries = Object.entries(data) for (let i = 0; i < entries.length; i++) { diff --git a/web/crux-ui/e2e/utils/projects.ts b/web/crux-ui/e2e/utils/projects.ts index 3c0bf4db22..d3be58a65c 100644 --- a/web/crux-ui/e2e/utils/projects.ts +++ b/web/crux-ui/e2e/utils/projects.ts @@ -69,7 +69,7 @@ export const createImage = async (page: Page, projectId: string, versionId: stri await page.waitForSelector('button:has-text("Add image")') - const settingsButton = await page.waitForSelector(`[src="/image_config_icon.svg"]:right-of(:text("${image}"))`) + const settingsButton = await page.waitForSelector(`[src="/container_config.svg"]:right-of(:text("${image}"))`) await settingsButton.click() await page.waitForSelector(`h2:has-text("Image")`) diff --git a/web/crux-ui/e2e/with-login/config-bundle.spec.ts b/web/crux-ui/e2e/with-login/config-bundle.spec.ts index 4c89a69c51..62751d66db 100644 --- a/web/crux-ui/e2e/with-login/config-bundle.spec.ts +++ b/web/crux-ui/e2e/with-login/config-bundle.spec.ts @@ -13,12 +13,14 @@ test('Creating a config bundle', async ({ page }) => { }) await page.goto(TEAM_ROUTES.configBundle.details(configBundleId)) + await page.waitForSelector(`h3:text-is("${BUNDLE_NAME}")`) + + await page.locator('button:has-text("Config")').click() + await page.waitForURL(TEAM_ROUTES.containerConfig.details('**')) const keyInput = page.locator('input[placeholder="Key"]').first() - await expect(keyInput).toBeDisabled() await expect(keyInput).toHaveValue(ENV_KEY) const valueInput = page.locator('input[placeholder="Value"]').first() - await expect(valueInput).toBeDisabled() await expect(valueInput).toHaveValue(ENV_VALUE) }) diff --git a/web/crux-ui/e2e/with-login/image-config/common-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts similarity index 69% rename from web/crux-ui/e2e/with-login/image-config/common-editor.spec.ts rename to web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts index 239b71fea1..6fa73895e5 100644 --- a/web/crux-ui/e2e/with-login/image-config/common-editor.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts @@ -1,5 +1,5 @@ +import { WS_TYPE_PATCH_CONFIG } from '@app/models' import { expect, Page } from '@playwright/test' -import { test } from '../../utils/test.fixture' import { NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES } from 'e2e/utils/common' import { createStorage } from 'e2e/utils/storages' import { @@ -20,37 +20,37 @@ import { wsPatchMatchVolume, } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' +import { test } from '../../utils/test.fixture' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { imageConfigId } } test.describe.configure({ mode: 'parallel' }) test.describe('Image common config from editor', () => { test('Container name should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'name-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'name-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const name = 'new-container-name' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchContainerName(name)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchContainerName(name)) await page.locator('input[placeholder="Container name"]').fill(name) await wsSent @@ -60,15 +60,15 @@ test.describe('Image common config from editor', () => { }) test('Expose strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'expose-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'expose-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchExpose('exposeWithTls')) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchExpose('exposeWithTls')) await page.getByRole('button', { name: 'HTTPS', exact: true }).click() await wsSent @@ -78,17 +78,17 @@ test.describe('Image common config from editor', () => { }) test('User should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'user-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'user-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const user = 23 - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchUser(user)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchUser(user)) await page.locator('input[placeholder="Container default"]').fill(user.toString()) await wsSent @@ -98,17 +98,17 @@ test.describe('Image common config from editor', () => { }) test('TTY should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'tty-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'tty-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("TTY")').click() - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchTTY(true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchTTY(true)) await page.locator('button[aria-checked="false"]:right-of(label:has-text("TTY"))').click() await wsSent @@ -118,13 +118,13 @@ test.describe('Image common config from editor', () => { }) test('Port should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Ports")').click() @@ -136,7 +136,7 @@ test.describe('Image common config from editor', () => { const internalInput = page.locator('input[placeholder="Internal"]') const externalInput = page.locator('input[placeholder="External"]') - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchPorts(internal, external)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchPorts(internal, external)) await internalInput.fill(internal) await externalInput.fill(external) await wsSent @@ -148,13 +148,13 @@ test.describe('Image common config from editor', () => { }) test('Port ranges should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-range-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-range-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Port ranges")').click() @@ -173,7 +173,7 @@ test.describe('Image common config from editor', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchPortRange(internalFrom, externalFrom, internalTo, externalTo), ) await internalInputFrom.fill(internalFrom) @@ -191,20 +191,20 @@ test.describe('Image common config from editor', () => { }) test('Secrets should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'secrets-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'secrets-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Secrets")').click() const secret = 'secretName' const secretInput = page.locator('input[placeholder="SECRETS"] >> visible=true').nth(0) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchSecret(secret, true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchSecret(secret, true)) await secretInput.fill(secret) await page.getByRole('switch', { checked: false }).locator(':right-of(:text("Required"))').click() @@ -217,20 +217,20 @@ test.describe('Image common config from editor', () => { }) test('Commands should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'commands-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'commands-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Commands")').click() const command = 'sleep' const commandInput = page.locator('input[placeholder="Commands"] >> visible=true').nth(0) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchCommand(command)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchCommand(command)) await commandInput.fill(command) await wsSent @@ -240,20 +240,20 @@ test.describe('Image common config from editor', () => { }) test('Arguments should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'arguments-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'arguments-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Arguments")').click() const argument = '1234' const argumentInput = page.locator('input[placeholder="Arguments"] >> visible=true').nth(0) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchArgument(argument)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchArgument(argument)) await argumentInput.fill(argument) await wsSent @@ -263,12 +263,12 @@ test.describe('Image common config from editor', () => { }) test('Routing should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'routing-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'routing-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Ports")').click() @@ -278,7 +278,7 @@ test.describe('Image common config from editor', () => { const internalInput = page.locator('input[placeholder="Internal"]') - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchPorts(internal)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchPorts(internal)) await internalInput.fill(internal) await wsSent @@ -293,7 +293,7 @@ test.describe('Image common config from editor', () => { wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchRouting(domain, path, uploadLimit, stripPath, routedPort), ) await page.locator('input[placeholder="Domain"]').fill(domain) @@ -314,24 +314,19 @@ test.describe('Image common config from editor', () => { }) test('Environment should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'environment-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'environment-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Environment")').click() const key = 'env-key' const value = 'env-value' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchEnvironment(key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchEnvironment(key, value)) await page.locator('input[placeholder="Key"]').first().fill(key) await page.locator('input[placeholder="Value"]').first().fill(value) await wsSent @@ -343,17 +338,12 @@ test.describe('Image common config from editor', () => { }) test('Config container should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'config-container-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'config-container-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Config container")').click() const confDiv = page.locator('div.grid:has(label:has-text("CONFIG CONTAINER"))') @@ -362,7 +352,7 @@ test.describe('Image common config from editor', () => { const volume = 'volume' const path = 'test/path/' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchConfigContainer(img, volume, path, true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchConfigContainer(img, volume, path, true)) await confDiv.getByLabel('Image').fill(img) await confDiv.getByLabel('Volume').fill(volume) await confDiv.getByLabel('Path').fill(path) @@ -378,17 +368,12 @@ test.describe('Image common config from editor', () => { }) test('Init containers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'init-container-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'init-container-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const name = 'container-name' const image = 'image' @@ -401,7 +386,7 @@ test.describe('Image common config from editor', () => { await page.locator('button:has-text("Init containers")').click() - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) await page.locator(`[src="/plus.svg"]:right-of(label:has-text("Init containers"))`).first().click() await wsSent @@ -410,7 +395,7 @@ test.describe('Image common config from editor', () => { wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchInitContainer(name, image, volName, volPath, arg, cmd, envKey, envVal), ) await confDiv.getByLabel('NAME').fill(name) @@ -436,16 +421,16 @@ test.describe('Image common config from editor', () => { }) test('Volume should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'volume-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'volume-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Volume")').click() - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) await page.locator(`[src="/plus.svg"]:right-of(label:has-text("Volume"))`).first().click() await wsSent @@ -454,7 +439,7 @@ test.describe('Image common config from editor', () => { const path = '/test/volume' const volumeClass = 'class' - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchVolume(name, size, path, volumeClass)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchVolume(name, size, path, volumeClass)) await page.getByLabel('Name').fill(name) await page.getByLabel('Size').fill(size) await page.getByLabel('Path').fill(path) @@ -470,20 +455,20 @@ test.describe('Image common config from editor', () => { }) test('Storage should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'storage-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'storage-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const storageName = 'image-editor-storage' const storageId = await createStorage(page, storageName, 'storage.com', '1234', '12345') const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Volume")').click() - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) await page.locator(`[src="/plus.svg"]:right-of(label:has-text("Volume"))`).first().click() await wsSent @@ -492,7 +477,7 @@ test.describe('Image common config from editor', () => { const path = '/storage/volume' const volumeClass = 'class' - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchVolume(volumeName, size, path, volumeClass)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchVolume(volumeName, size, path, volumeClass)) await page.getByLabel('Name').fill(volumeName) await page.getByLabel('Size').fill(size) await page.getByLabel('Path').fill(path) @@ -503,7 +488,7 @@ test.describe('Image common config from editor', () => { const bucketPath = '/storage/' await page.locator('button:has-text("Storage")').click() - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchStorage(storageId, bucketPath, volumeName)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchStorage(storageId, bucketPath, volumeName)) await storageDiv.locator(`button:has-text("${storageName}")`).click() await storageDiv.locator('input[placeholder="Bucket path"]').fill(bucketPath) await storageDiv.locator(`button:has-text("${volumeName}")`).click() diff --git a/web/crux-ui/e2e/with-login/image-config/common-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/common-json.spec.ts similarity index 68% rename from web/crux-ui/e2e/with-login/image-config/common-json.spec.ts rename to web/crux-ui/e2e/with-login/container-config/common-json.spec.ts index a3a91d5f2c..5432be017b 100644 --- a/web/crux-ui/e2e/with-login/image-config/common-json.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/common-json.spec.ts @@ -21,32 +21,32 @@ import { } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { imageConfigId } } test.describe.configure({ mode: 'parallel' }) test.describe('Image common config from JSON', () => { test('Container name should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'name-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'name-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const name = 'new-container-name' @@ -57,7 +57,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.name = name - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchContainerName(name)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchContainerName(name)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -67,13 +67,13 @@ test.describe('Image common config from JSON', () => { }) test('Expose strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'expose-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'expose-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -82,7 +82,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.expose = 'exposeWithTls' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchExpose('exposeWithTls')) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchExpose('exposeWithTls')) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -92,13 +92,13 @@ test.describe('Image common config from JSON', () => { }) test('User should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'user-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'user-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const user = 23 @@ -109,7 +109,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.user = user - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchUser(user)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchUser(user)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -119,13 +119,13 @@ test.describe('Image common config from JSON', () => { }) test('TTY should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'tty-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'tty-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -134,7 +134,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.tty = true - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchTTY(true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchTTY(true)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -144,13 +144,13 @@ test.describe('Image common config from JSON', () => { }) test('Port should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -164,7 +164,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.ports = [{ internal: internalAsNumber, external: externalAsNumber }] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchPorts(internal, external)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchPorts(internal, external)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -178,13 +178,13 @@ test.describe('Image common config from JSON', () => { }) test('Port ranges should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-range-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-range-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -210,7 +210,7 @@ test.describe('Image common config from JSON', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchPortRange(internalFrom, externalFrom, internalTo, externalTo), ) await jsonEditor.fill(JSON.stringify(json)) @@ -230,13 +230,13 @@ test.describe('Image common config from JSON', () => { }) test('Secrets should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'secrets-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'secrets-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const secret = 'secretName' const secretInput = page.locator('input[placeholder="SECRETS"] >> visible=true').nth(0) @@ -248,7 +248,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.secrets = [{ key: secret, required: true }] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchSecret(secret, true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchSecret(secret, true)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -259,13 +259,13 @@ test.describe('Image common config from JSON', () => { }) test('Commands should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'commands-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'commands-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const command = 'sleep' @@ -276,7 +276,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.commands = [command] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchCommand(command)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchCommand(command)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -286,13 +286,13 @@ test.describe('Image common config from JSON', () => { }) test('Arguments should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'arguments-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'arguments-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const argument = '1234' @@ -303,7 +303,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.args = [argument] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchArgument(argument)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchArgument(argument)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -313,12 +313,12 @@ test.describe('Image common config from JSON', () => { }) test('Routing should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'routing-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'routing-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -337,7 +337,7 @@ test.describe('Image common config from JSON', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchRouting(domain, path, uploadLimit, stripPath, port), ) await jsonEditor.fill(JSON.stringify(json)) @@ -352,12 +352,12 @@ test.describe('Image common config from JSON', () => { }) test('Environment should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'environment-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'environment-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'env-key' const value = 'env-value' @@ -369,7 +369,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.environment = { [key]: value } - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchEnvironment(key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchEnvironment(key, value)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -380,17 +380,12 @@ test.describe('Image common config from JSON', () => { }) test('Config container should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'config-container-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'config-container-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const img = 'image' const volume = 'volume' @@ -403,7 +398,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.configContainer = { image: img, volume, path, keepFiles: true } - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchConfigContainer(img, volume, path, true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchConfigContainer(img, volume, path, true)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -417,17 +412,12 @@ test.describe('Image common config from JSON', () => { }) test('Init containers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'init-container-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'init-container-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const name = 'container-name' const image = 'image' @@ -458,7 +448,7 @@ test.describe('Image common config from JSON', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchInitContainer(name, image, volName, volPath, arg, cmd, envKey, envVal), ) await jsonEditor.fill(JSON.stringify(json)) @@ -478,12 +468,12 @@ test.describe('Image common config from JSON', () => { }) test('Volume should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'volume-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'volume-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -497,7 +487,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.volumes = [{ name, path, type: 'rwo', class: volumeClass, size }] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchVolume(name, size, path, volumeClass)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchVolume(name, size, path, volumeClass)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -510,9 +500,9 @@ test.describe('Image common config from JSON', () => { }) test('Storage should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'storage-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + const { imageConfigId } = await setup(page, 'storage-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const volumeName = 'volume-name' const size = '1024' @@ -523,10 +513,10 @@ test.describe('Image common config from JSON', () => { const storageId = await createStorage(page, storageName, 'storage.com', '1234', '12345') const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -536,7 +526,12 @@ test.describe('Image common config from JSON', () => { json.volumes = [{ name: volumeName, path, type: 'rwo', class: volumeClass, size }] json.storage = { storageId, bucket: bucketPath, path: volumeName } - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchStorage(storageId, bucketPath, volumeName)) + const wsSent = wsPatchSent( + ws, + wsRoute, + WS_TYPE_PATCH_CONFIG, + wsPatchMatchStorage(storageId, bucketPath, volumeName), + ) await jsonEditor.fill(JSON.stringify(json)) await wsSent diff --git a/web/crux-ui/e2e/with-login/image-config/image-config-filters.spec.ts b/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts similarity index 66% rename from web/crux-ui/e2e/with-login/image-config/image-config-filters.spec.ts rename to web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts index df1cae48d3..fbf3249a50 100644 --- a/web/crux-ui/e2e/with-login/image-config/image-config-filters.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts @@ -1,30 +1,26 @@ import { expect, Page } from '@playwright/test' -import { test } from '../../utils/test.fixture' import { NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES } from 'e2e/utils/common' import { createImage, createProject, createVersion } from '../../utils/projects' +import { test } from '../../utils/test.fixture' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { - projectId, - versionId, - imageId, - } + return { imageConfigId } } test.describe('Filters', () => { test('None should be selected by default', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'filter-all', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'filter-all', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const allButton = await page.locator('button:has-text("All")') @@ -34,9 +30,9 @@ test.describe('Filters', () => { }) test('All should not be selected if one of the main filters are not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'filter-select', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'filter-select', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') await page.locator(`button:has-text("Common")`).first().click() @@ -47,9 +43,9 @@ test.describe('Filters', () => { }) test('Main filter should not be selected if one of its sub filters are not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'sub-filter', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'sub-filter', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const subFilter = await page.locator(`button:has-text("Network mode")`) @@ -62,9 +58,9 @@ test.describe('Filters', () => { }) test('Config field should be invisible if its sub filter is not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'sub-deselect', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'sub-deselect', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const subFilter = await page.locator(`button:has-text("Deployment strategy")`) diff --git a/web/crux-ui/e2e/with-login/image-config/docker-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts similarity index 63% rename from web/crux-ui/e2e/with-login/image-config/docker-editor.spec.ts rename to web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts index 810c454908..7a68cc2e84 100644 --- a/web/crux-ui/e2e/with-login/image-config/docker-editor.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts @@ -10,38 +10,33 @@ import { wsPatchMatchRestartPolicy, } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { imageConfigId } } test.describe('Image docker config from editor', () => { test('Network mode should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'networkmode-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'networkmode-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const mode = 'host' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchNetworkMode(mode)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchNetworkMode(mode)) await page.locator(`div.grid:has(label:has-text("NETWORK MODE")) button:has-text("${mode}")`).click() await wsSent @@ -53,17 +48,12 @@ test.describe('Image docker config from editor', () => { }) test('Docker labels should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'dockerlabel-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'dockerlabel-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'docker-key' const value = 'docker-value' @@ -72,7 +62,7 @@ test.describe('Image docker config from editor', () => { await page.locator('button:has-text("Docker labels")').click() - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDockerLabel(key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDockerLabel(key, value)) await keyInput.fill(key) await valueInput.fill(value) await wsSent @@ -84,19 +74,14 @@ test.describe('Image docker config from editor', () => { }) test('Restart policy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'restartpolicy-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'restartpolicy-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchRestartPolicy('always')) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchRestartPolicy('always')) await page.locator('div.grid:has(label:has-text("RESTART POLICY")) button:has-text("Always")').click() await wsSent @@ -108,12 +93,12 @@ test.describe('Image docker config from editor', () => { }) test('Log config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'logconfig-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'logconfig-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Log config")').click() @@ -123,7 +108,7 @@ test.describe('Image docker config from editor', () => { const loggerConf = page.locator('div.grid:has(label:has-text("LOG CONFIG"))') - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLogConfig(type, key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLogConfig(type, key, value)) await loggerConf.locator('input[placeholder="Key"]').first().fill(key) await loggerConf.locator('input[placeholder="Value"]').first().fill(value) await loggerConf.locator(`button:has-text("${type}")`).click() @@ -137,18 +122,18 @@ test.describe('Image docker config from editor', () => { }) test('Networks should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'networks-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'networks-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Networks")').click() const network = '10.16.128.196' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchNetwork(network)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchNetwork(network)) await page.locator('div.grid:has(label:has-text("NETWORKS")) input[placeholder="Network"]').first().fill(network) await wsSent diff --git a/web/crux-ui/e2e/with-login/image-config/docker-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts similarity index 68% rename from web/crux-ui/e2e/with-login/image-config/docker-json.spec.ts rename to web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts index 9ba93f576f..ab69528e8b 100644 --- a/web/crux-ui/e2e/with-login/image-config/docker-json.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts @@ -10,31 +10,31 @@ import { wsPatchMatchRestartPolicy, } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { imageConfigId } } test.describe.configure({ mode: 'parallel' }) test.describe('Image docker config from JSON', () => { test('Network mode should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'networkmode-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'networkmode-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const mode = 'host' @@ -45,7 +45,7 @@ test.describe('Image docker config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.networkMode = mode - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchNetworkMode(mode)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchNetworkMode(mode)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -57,12 +57,12 @@ test.describe('Image docker config from JSON', () => { }) test('Docker labels should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'dockerlabel-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'dockerlabel-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'docker-key' const value = 'docker-value' @@ -74,7 +74,7 @@ test.describe('Image docker config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.dockerLabels = { [key]: value } - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDockerLabel(key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDockerLabel(key, value)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -89,17 +89,12 @@ test.describe('Image docker config from JSON', () => { }) test('Restart policy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'restartpolicy-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'restartpolicy-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -108,7 +103,7 @@ test.describe('Image docker config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.restartPolicy = 'always' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchRestartPolicy('always')) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchRestartPolicy('always')) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -120,12 +115,12 @@ test.describe('Image docker config from JSON', () => { }) test('Log config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'logconfig-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'logconfig-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const type = 'json-file' const key = 'logger-key' @@ -138,7 +133,7 @@ test.describe('Image docker config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.logConfig = { driver: type, options: { [key]: value } } - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLogConfig(type, key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLogConfig(type, key, value)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -151,12 +146,12 @@ test.describe('Image docker config from JSON', () => { }) test('Networks should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'networks-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'networks-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Networks")').click() @@ -169,7 +164,7 @@ test.describe('Image docker config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.networks = [network] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchNetwork(network)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchNetwork(network)) await jsonEditor.fill(JSON.stringify(json)) await wsSent diff --git a/web/crux-ui/e2e/with-login/image-config/image-config-view-state.spec.ts b/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts similarity index 69% rename from web/crux-ui/e2e/with-login/image-config/image-config-view-state.spec.ts rename to web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts index 1a9c3e35d8..414acbf567 100644 --- a/web/crux-ui/e2e/with-login/image-config/image-config-view-state.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts @@ -8,23 +8,19 @@ const setup = async ( projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { - projectId, - versionId, - imageId, - } + return { imageConfigId } } test.describe('View state', () => { test('Editor state should show the configuration fields', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'editor-state-conf', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'editor-state-conf', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const editorButton = await page.waitForSelector('button:has-text("Editor")') @@ -39,9 +35,9 @@ test.describe('View state', () => { }) test('JSON state should show the json editor', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'editor-state-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'editor-state-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') diff --git a/web/crux-ui/e2e/with-login/image-config/kubernetes-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts similarity index 70% rename from web/crux-ui/e2e/with-login/image-config/kubernetes-editor.spec.ts rename to web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts index 5a6cd1a3ff..972d7a16f7 100644 --- a/web/crux-ui/e2e/with-login/image-config/kubernetes-editor.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts @@ -18,39 +18,34 @@ import { } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { imageConfigId } } test.describe('Image kubernetes config from editor', () => { test('Deployment strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'deployment-strategy-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'deployment-strategy-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const strategy = 'rolling' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentStrategy(strategy)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentStrategy(strategy)) await page.locator(`button:has-text("${strategy}")`).click() await wsSent @@ -60,18 +55,13 @@ test.describe('Image kubernetes config from editor', () => { }) test('Custom headers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'custom-headers-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'custom-headers-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Custom headers")').click() @@ -80,7 +70,7 @@ test.describe('Image kubernetes config from editor', () => { .locator('div.grid:has(label:has-text("CUSTOM HEADERS")) input[placeholder="Header name"]') .first() - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchCustomHeader(header)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchCustomHeader(header)) await input.fill(header) await wsSent @@ -90,22 +80,17 @@ test.describe('Image kubernetes config from editor', () => { }) test('Proxy headers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'proxy-headers-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'proxy-headers-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Proxy headers")').click() - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchProxyHeader(true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchProxyHeader(true)) await page.locator('button[aria-checked="false"]:right-of(label:has-text("PROXY HEADERS"))').click() await wsSent @@ -115,29 +100,24 @@ test.describe('Image kubernetes config from editor', () => { }) test('Load balancer should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'load-balancer-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'load-balancer-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Use load balancer")').click() const key = 'balancer-key' const value = 'balancer-value' - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLoadBalancer(true)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLoadBalancer(true)) await page.locator('button[aria-checked="false"]:right-of(label:has-text("USE LOAD BALANCER"))').click() await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLBAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLBAnnotations(key, value)) await page.locator('div.grid:has(label:has-text("USE LOAD BALANCER")) input[placeholder="Key"]').first().fill(key) await page .locator('div.grid:has(label:has-text("USE LOAD BALANCER")) input[placeholder="Value"]') @@ -159,18 +139,13 @@ test.describe('Image kubernetes config from editor', () => { }) test('Health check config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'health-check-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'health-check-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Health check config")').click() @@ -184,7 +159,7 @@ test.describe('Image kubernetes config from editor', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchHealthCheck(port, liveness, readiness, startup), ) await hcConf.locator('input[placeholder="Port"]').fill(port.toString()) @@ -202,17 +177,12 @@ test.describe('Image kubernetes config from editor', () => { }) test('Resource config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'resource-config-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'resource-config-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Resource config")').click() @@ -226,7 +196,7 @@ test.describe('Image kubernetes config from editor', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchResourceConfig(cpuLimits, cpuRequests, memoryLimits, memoryRequests), ) await rsConf.locator('input').nth(0).fill(cpuLimits) @@ -247,13 +217,13 @@ test.describe('Image kubernetes config from editor', () => { page.locator(`div.max-h-128 > div:nth-child(2):near(label:has-text("${category}"))`) test('Labels should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'labels-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'labels-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.getByRole('button', { name: 'Labels', exact: true }).click() @@ -264,15 +234,15 @@ test.describe('Image kubernetes config from editor', () => { const serviceDiv = await getCategoryDiv('Service', page) const ingressDiv = await getCategoryDiv('Ingress', page) - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentLabel(key, value)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentLabel(key, value)) await deploymentDiv.locator('input[placeholder="Key"]').first().fill(key) await deploymentDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchServiceLabel(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchServiceLabel(key, value)) await serviceDiv.locator('input[placeholder="Key"]').first().fill(key) await serviceDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchIngressLabel(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchIngressLabel(key, value)) await ingressDiv.locator('input[placeholder="Key"]').first().fill(key) await ingressDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent @@ -288,18 +258,13 @@ test.describe('Image kubernetes config from editor', () => { }) test('Annotations should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'annotations-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'annotations-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.getByRole('button', { name: 'Annotations', exact: true }).click() @@ -310,15 +275,15 @@ test.describe('Image kubernetes config from editor', () => { const serviceDiv = await getCategoryDiv('Service', page) const ingressDiv = await getCategoryDiv('Ingress', page) - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentAnnotations(key, value)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentAnnotations(key, value)) await deploymentDiv.locator('input[placeholder="Key"]').first().fill(key) await deploymentDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchServiceAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchServiceAnnotations(key, value)) await serviceDiv.locator('input[placeholder="Key"]').first().fill(key) await serviceDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchIngressAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchIngressAnnotations(key, value)) await ingressDiv.locator('input[placeholder="Key"]').first().fill(key) await ingressDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent diff --git a/web/crux-ui/e2e/with-login/image-config/kubernetes-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts similarity index 72% rename from web/crux-ui/e2e/with-login/image-config/kubernetes-json.spec.ts rename to web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts index 9fa54071af..3e372bf41b 100644 --- a/web/crux-ui/e2e/with-login/image-config/kubernetes-json.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts @@ -1,5 +1,5 @@ +import { WS_TYPE_PATCH_CONFIG } from '@app/models' import { expect, Page } from '@playwright/test' -import { test } from '../../utils/test.fixture' import { NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES } from 'e2e/utils/common' import { wsPatchMatchCustomHeader, @@ -17,36 +17,31 @@ import { wsPatchMatchServiceLabel, } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' +import { test } from '../../utils/test.fixture' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { imageConfigId } } test.describe('Image kubernetes config from JSON', () => { test('Deployment strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'deployment-strategy-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'deployment-strategy-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const strategy = 'rolling' @@ -57,7 +52,7 @@ test.describe('Image kubernetes config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.deploymentStrategy = strategy - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentStrategy(strategy)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentStrategy(strategy)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -67,18 +62,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Custom headers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'custom-headers-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'custom-headers-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const header = 'test-header' @@ -89,7 +79,7 @@ test.describe('Image kubernetes config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.customHeaders = [header] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchCustomHeader(header)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchCustomHeader(header)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -102,18 +92,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Proxy headers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'proxy-headers-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'proxy-headers-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -122,7 +107,7 @@ test.describe('Image kubernetes config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.proxyHeaders = true - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchProxyHeader(true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchProxyHeader(true)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -132,18 +117,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Load balancer should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'load-balancer-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'load-balancer-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'balancer-key' const value = 'balancer-value' @@ -155,11 +135,11 @@ test.describe('Image kubernetes config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.useLoadBalancer = true - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLoadBalancer(true)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLoadBalancer(true)) await jsonEditor.fill(JSON.stringify(json)) await wsSent json.extraLBAnnotations = { [key]: value } - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLBAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLBAnnotations(key, value)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -177,13 +157,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Health check config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'health-check-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'health-check-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const port = 12560 const liveness = 'test/liveness/' @@ -200,7 +180,7 @@ test.describe('Image kubernetes config from JSON', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchHealthCheck(port, liveness, readiness, startup), ) await jsonEditor.fill(JSON.stringify(json)) @@ -216,18 +196,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Resource config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'resource-config-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'resource-config-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const cpuLimits = '50' const cpuRequests = '25' @@ -247,7 +222,7 @@ test.describe('Image kubernetes config from JSON', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchResourceConfig(cpuLimits, cpuRequests, memoryLimits, memoryRequests), ) await jsonEditor.fill(JSON.stringify(json)) @@ -266,13 +241,13 @@ test.describe('Image kubernetes config from JSON', () => { page.locator(`div.max-h-128 > div:nth-child(2):near(label:has-text("${category}"))`) test('Labels should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'labels-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'labels-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'label-key' const value = 'label-value' @@ -283,15 +258,15 @@ test.describe('Image kubernetes config from JSON', () => { const jsonEditor = await page.locator('textarea') const json = JSON.parse(await jsonEditor.inputValue()) - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentLabel(key, value)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentLabel(key, value)) json.labels = { deployment: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchServiceLabel(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchServiceLabel(key, value)) json.labels = { deployment: { [key]: value }, service: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchIngressLabel(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchIngressLabel(key, value)) json.labels = { deployment: { [key]: value }, service: { [key]: value }, ingress: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -310,13 +285,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Annotations should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'annotations-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'annotations-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'annotation-key' const value = 'annotation-value' @@ -327,15 +302,15 @@ test.describe('Image kubernetes config from JSON', () => { const jsonEditor = await page.locator('textarea') const json = JSON.parse(await jsonEditor.inputValue()) - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentAnnotations(key, value)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentAnnotations(key, value)) json.annotations = { deployment: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchServiceAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchServiceAnnotations(key, value)) json.annotations = { deployment: { [key]: value }, service: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchIngressAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchIngressAnnotations(key, value)) json.annotations = { deployment: { [key]: value }, service: { [key]: value }, ingress: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts index 23fdddb7f3..d3a5111d0c 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts @@ -1,4 +1,4 @@ -import { WS_TYPE_PATCH_IMAGE, WS_TYPE_PATCH_INSTANCE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_INSTANCE } from '@app/models' import { Page, expect } from '@playwright/test' import { wsPatchMatchEverySecret, wsPatchMatchNonNullSecretValues } from 'e2e/utils/websocket-match' import { DAGENT_NODE, NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES, waitForURLExcept } from '../../utils/common' @@ -21,10 +21,10 @@ const addSecretToImage = async ( secretKeys: string[], ): Promise => { const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -33,7 +33,7 @@ const addSecretToImage = async ( const json = JSON.parse(await jsonEditor.inputValue()) json.secrets = secretKeys.map(key => ({ key, required: false })) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchEverySecret(secretKeys)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchEverySecret(secretKeys)) await jsonEditor.fill(JSON.stringify(json)) await wsSent } @@ -44,7 +44,7 @@ const openContainerConfigByDeploymentTable = async (page: Page, containerName: s await expect(page.locator(`td:has-text("${containerName}")`).first()).toBeVisible() const containerSettingsButton = await page.waitForSelector( - `[src="/instance_config_icon.svg"]:right-of(:text("${containerName}"))`, + `[src="/concrete_container_config.svg"]:right-of(:text("${containerName}"))`, ) await containerSettingsButton.click() @@ -73,9 +73,9 @@ test.describe('Deployment Copy', () => { await createNode(page, newNodeName) const versionId = await createVersion(page, projectId, '0.1.0', 'Incremental') - const imageId = await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) + const imageConfigId = await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) - await addSecretToImage(page, projectId, versionId, imageId, secretKeys) + await addSecretToImage(page, projectId, versionId, imageConfigId, secretKeys) const { id: deploymentId } = await addDeploymentToVersion(page, projectId, versionId, DAGENT_NODE, { prefix: originalPrefix, @@ -116,7 +116,7 @@ test.describe('Deployment Copy', () => { await copyButton.click() const newPrefix = 'dcpy-second' - await page.locator(`button:has-text("${DAGENT_NODE}")`).click() + await page.locator(`button:has-text("${DAGENT_NODE}")`).first().click() await fillDeploymentPrefix(page, newPrefix) const currentUrl = page.url() diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts index 39b799f2d2..fa1b4969ab 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts @@ -75,7 +75,7 @@ test.describe('Versioned Project', () => { const copyButton = await page.locator('button:has-text("Copy")') await copyButton.click() - await page.locator(`button:has-text("${nodeName}")`).click() + await page.locator(`button:has-text("${nodeName}")`).first().click() await fillDeploymentPrefix(page, `${prefix}-new-prefix`) const currentUrl = page.url() @@ -102,7 +102,7 @@ test.describe('Versioned Project', () => { const copyButton = await page.locator(`[alt="Copy"]:right-of(:has-text("${projectName}"))`).first() await copyButton.click() - await page.locator(`button:has-text("${nodeName}")`).click() + await page.locator(`button:has-text("${nodeName}")`).first().click() await fillDeploymentPrefix(page, prefix) await page.locator('button:has-text("Copy")').click() @@ -124,7 +124,7 @@ test.describe('Versioned Project', () => { await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const editorButton = await page.waitForSelector('button:has-text("JSON")') await editorButton.click() diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versionless.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versionless.spec.ts index 16f8b1fbb1..fb0c11d3ef 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versionless.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versionless.spec.ts @@ -41,7 +41,7 @@ test.describe('Versionless Project', () => { const copyButton = await page.locator(`[alt="Copy"]:right-of(:has-text("${projectName}"))`).first() await copyButton.click() - await page.locator(`button:has-text("${nodeName}")`).click() + await page.locator(`button:has-text("${nodeName}")`).first().click() await fillDeploymentPrefix(page, prefix) await page.locator('button:has-text("Copy")').click() @@ -96,7 +96,7 @@ test.describe('Versionless Project', () => { const copyButton = page.locator('button:has-text("Copy")') await copyButton.click() - await page.locator(`button:has-text("${nodeName}")`).click() + await page.locator(`button:has-text("${nodeName}")`).first().click() await fillDeploymentPrefix(page, `${prefix}-new-prefix`) const currentUrl = page.url() diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts index 818ac7305d..7d605877c3 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts @@ -4,20 +4,20 @@ import { NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES } from 'e2e/utils/common' import { deployWithDagent } from '../../utils/node-helper' import { addImageToVersion, createImage, createProject, createVersion } from '../../utils/projects' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' test('In progress deployment should be not deletable', async ({ page }) => { const projectName = 'project-delete-test-1' const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, '0.1.0', 'Incremental') - const imageId = await createImage(page, projectId, versionId, 'nginx') + const imageConfigId = await createImage(page, projectId, versionId, 'nginx') const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const editorButton = await page.waitForSelector('button:has-text("JSON")') await editorButton.click() @@ -38,7 +38,7 @@ test('In progress deployment should be not deletable', async ({ page }) => { useParentConfig: false, }, ] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) await jsonContainer.fill(JSON.stringify(configObject)) await wsSent diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versioned.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versioned.spec.ts index ae42ce4b5a..853ec75928 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versioned.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versioned.spec.ts @@ -1,8 +1,8 @@ import { expect } from '@playwright/test' -import { test } from '../../utils/test.fixture' import { DAGENT_NODE, TEAM_ROUTES } from '../../utils/common' import { deployWithDagent } from '../../utils/node-helper' import { addDeploymentToVersion, createImage, createProject, createVersion } from '../../utils/projects' +import { test } from '../../utils/test.fixture' const image = 'nginx' @@ -19,7 +19,9 @@ test.describe('Versioned Project incremental version', () => { await expect(await page.locator('button:has-text("Edit")')).toHaveCount(1) - const configButton = await page.locator(`[src="/instance_config_icon.svg"]:right-of(:has-text("${image}"))`).first() + const configButton = await page + .locator(`[src="/concrete_container_config.svg"]:right-of(:has-text("${image}"))`) + .first() await configButton.click() await page.waitForSelector('input[id="common.containerName"]') @@ -45,7 +47,9 @@ test.describe('Versioned Project incremental version', () => { await expect(await page.locator('button:has-text("Edit")')).toHaveCount(0) - const configButton = await page.locator(`[src="/instance_config_icon.svg"]:right-of(:has-text("${image}"))`).first() + const configButton = await page + .locator(`[src="/concrete_container_config.svg"]:right-of(:has-text("${image}"))`) + .first() await configButton.click() await page.waitForSelector('input[id="common.containerName"]') @@ -73,7 +77,9 @@ test.describe('Versioned Project incremental version', () => { await expect(await page.locator('button:has-text("Edit")')).toHaveCount(0) - const configButton = await page.locator(`[src="/instance_config_icon.svg"]:right-of(:has-text("${image}"))`).first() + const configButton = await page + .locator(`[src="/concrete_container_config.svg"]:right-of(:has-text("${image}"))`) + .first() await configButton.click() await page.waitForSelector('input[id="common.containerName"]') diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versionless.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versionless.spec.ts index 90f74364dc..7f0e925ee9 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versionless.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versionless.spec.ts @@ -1,8 +1,8 @@ import { expect } from '@playwright/test' -import { test } from '../../utils/test.fixture' import { DAGENT_NODE, screenshotPath, TEAM_ROUTES } from '../../utils/common' import { deployWithDagent } from '../../utils/node-helper' import { addDeploymentToVersionlessProject, addImageToVersionlessProject, createProject } from '../../utils/projects' +import { test } from '../../utils/test.fixture' const image = 'nginx' @@ -14,7 +14,9 @@ test.describe('Versionless Project', () => { await expect(await page.locator('button:has-text("Edit")')).toHaveCount(1) - const configButton = await page.locator(`[src="/instance_config_icon.svg"]:right-of(:has-text("${image}"))`).first() + const configButton = await page + .locator(`[src="/concrete_container_config.svg"]:right-of(:has-text("${image}"))`) + .first() await configButton.click() await page.waitForSelector('input[id="common.containerName"]') @@ -36,7 +38,9 @@ test.describe('Versionless Project', () => { await expect(await page.locator('button:has-text("Edit")')).toHaveCount(1) - const configButton = await page.locator(`[src="/instance_config_icon.svg"]:right-of(:has-text("${image}"))`).first() + const configButton = await page + .locator(`[src="/concrete_container_config.svg"]:right-of(:has-text("${image}"))`) + .first() await configButton.click() await page.waitForSelector('input[id="common.containerName"]') diff --git a/web/crux-ui/e2e/with-login/deployment/deployment.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment.spec.ts index ea1a6101e7..88752c2550 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment.spec.ts @@ -110,7 +110,7 @@ test('Can create from deployments page', async ({ page }) => { await page.locator('button:has-text("Add")').click() await expect(page.locator('h4:has-text("Add")')).toBeVisible() - await page.locator(`button:has-text("${DAGENT_NODE}")`).click() + await page.locator(`button:has-text("${DAGENT_NODE}")`).first().click() await page.locator(`button:has-text("${projectName}")`).click() await page.locator(`button:has-text("${versionName}")`).click() await fillDeploymentPrefix(page, projectName.toLowerCase()) diff --git a/web/crux-ui/e2e/with-login/image-config.spec.ts b/web/crux-ui/e2e/with-login/image-config.spec.ts index 88d17dcd28..eb009625d5 100644 --- a/web/crux-ui/e2e/with-login/image-config.spec.ts +++ b/web/crux-ui/e2e/with-login/image-config.spec.ts @@ -1,32 +1,32 @@ +import { WS_TYPE_PATCH_CONFIG } from '@app/models' import { expect, Page } from '@playwright/test' -import { test } from '../utils/test.fixture' import { NGINX_TEST_IMAGE_WITH_TAG, screenshotPath, TEAM_ROUTES } from '../utils/common' import { createImage, createProject, createVersion } from '../utils/projects' +import { test } from '../utils/test.fixture' import { waitSocketRef, wsPatchSent } from '../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ projectId: string; versionId: string; imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) return { projectId, versionId, - imageId, + imageConfigId, } } test.describe('View state', () => { test('Editor state should show the configuration fields', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'editor-state-conf', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'editor-state-conf', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const editorButton = await page.waitForSelector('button:has-text("Editor")') @@ -40,9 +40,9 @@ test.describe('View state', () => { }) test('JSON state should show the json editor', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'editor-state-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'editor-state-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') @@ -57,9 +57,9 @@ test.describe('View state', () => { test.describe('Filters', () => { test('None should be selected by default', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'filter-all', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'filter-all', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const allButton = await page.locator('button:has-text("All")') @@ -69,9 +69,9 @@ test.describe('Filters', () => { }) test('All should not be selected if one of the main filters are not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'filter-select', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'filter-select', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') await page.locator(`button:has-text("Common")`).first().click() @@ -82,9 +82,9 @@ test.describe('Filters', () => { }) test('Main filter should not be selected if one of its sub filters are not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'sub-filter', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'sub-filter', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const subFilter = await page.locator(`button:has-text("Network mode")`) @@ -96,9 +96,9 @@ test.describe('Filters', () => { }) test('Config field should be invisible if its sub filter is not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'sub-deselect', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'sub-deselect', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const subFilter = await page.locator(`button:has-text("Deployment strategy")`) @@ -119,17 +119,17 @@ const wsPatchMatchPorts = (internalPort: string, externalPort?: string) => (payl test.describe('Image configurations', () => { test('Port should be saved after adding it from the config field', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Ports")').click() - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) const addPortsButton = await page.locator(`[src="/plus.svg"]:right-of(label:has-text("Ports"))`).first() await addPortsButton.click() await wsSent @@ -140,7 +140,7 @@ test.describe('Image configurations', () => { const internalInput = page.locator('input[placeholder="Internal"]') const externalInput = page.locator('input[placeholder="External"]') - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchPorts(internal, external)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchPorts(internal, external)) await internalInput.type(internal) await externalInput.type(external) await wsSent @@ -152,13 +152,13 @@ test.describe('Image configurations', () => { }) test('Port should be saved after adding it from the json editor', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -172,7 +172,7 @@ test.describe('Image configurations', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.ports = [{ internal: internalAsNumber, external: externalAsNumber }] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchPorts(internal, external)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchPorts(internal, external)) await jsonEditor.fill(JSON.stringify(json)) await wsSent diff --git a/web/crux-ui/e2e/with-login/nodes.spec.ts b/web/crux-ui/e2e/with-login/nodes.spec.ts index 4430e041e9..3e12d0181f 100644 --- a/web/crux-ui/e2e/with-login/nodes.spec.ts +++ b/web/crux-ui/e2e/with-login/nodes.spec.ts @@ -235,8 +235,13 @@ test('Stopping the underlying container of a log stream should not affect the co TEAM_ROUTES.node.containerLog(nodeId, { name: containerName, }), + { + waitUntil: 'domcontentloaded', + }, ) + await page.waitForSelector(`h4:text-is("Log of ${containerName}")`) + await stopContainer(containerName) // check status diff --git a/web/crux-ui/e2e/with-login/resource-copy.spec.ts b/web/crux-ui/e2e/with-login/resource-copy.spec.ts index 42730f7585..9f1c9fd048 100644 --- a/web/crux-ui/e2e/with-login/resource-copy.spec.ts +++ b/web/crux-ui/e2e/with-login/resource-copy.spec.ts @@ -1,4 +1,4 @@ -import { WS_TYPE_PATCH_IMAGE, WS_TYPE_PATCH_INSTANCE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_INSTANCE } from '@app/models' import { expect } from '@playwright/test' import { DAGENT_NODE, NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES, waitForURLExcept } from 'e2e/utils/common' import { addPortsToContainerConfig } from 'e2e/utils/container-config' @@ -51,17 +51,17 @@ test.describe('Deleting default version', () => { const projectId = await createProject(page, projectName, 'versioned') const defaultVersionName = 'default-version' const defaultVersionId = await createVersion(page, projectId, defaultVersionName, 'Rolling') - const defaultVersionImageId = await createImage(page, projectId, defaultVersionId, NGINX_TEST_IMAGE_WITH_TAG) + const imageConfigId = await createImage(page, projectId, defaultVersionId, NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(defaultVersionId, defaultVersionImageId)) + await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(defaultVersionId, imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(defaultVersionId) const internal = '1000' const external = '2000' - await addPortsToContainerConfig(page, ws, wsRoute, WS_TYPE_PATCH_IMAGE, internal, external) + await addPortsToContainerConfig(page, ws, wsRoute, WS_TYPE_PATCH_CONFIG, internal, external) const newVersionId = await createVersion(page, projectId, 'new-version', 'Rolling') @@ -80,7 +80,7 @@ test.describe('Deleting default version', () => { await expect(imagesRows).toHaveCount(1) await expect(page.locator('td:has-text("nginx")').first()).toBeVisible() - const settingsButton = await page.waitForSelector(`[src="/image_config_icon.svg"]:right-of(:text("nginx"))`) + const settingsButton = await page.waitForSelector(`[src="/container_config.svg"]:right-of(:text("nginx"))`) await settingsButton.click() await page.waitForSelector(`h2:has-text("Image")`) @@ -185,7 +185,7 @@ test.describe('Deleting default version', () => { await expect(instancesRows).toHaveCount(1) await expect(page.locator('td:has-text("nginx")').first()).toBeVisible() - const settingsButton = await page.waitForSelector(`[src="/instance_config_icon.svg"]:right-of(:text("nginx"))`) + const settingsButton = await page.waitForSelector(`[src="/concrete_container_config.svg"]:right-of(:text("nginx"))`) await settingsButton.click() await page.waitForSelector(`h2:has-text("Container")`) @@ -224,7 +224,7 @@ test.describe('Deleting default version', () => { await expect(page.locator('td:has-text("nginx")').first()).toBeVisible() const newVersionDeploymentSettingsButton = await page.waitForSelector( - `[src="/instance_config_icon.svg"]:right-of(:text("nginx"))`, + `[src="/concrete_container_config.svg"]:right-of(:text("nginx"))`, ) await newVersionDeploymentSettingsButton.click() @@ -284,7 +284,7 @@ test.describe("Deleting copied deployment's parent", () => { await expect(instancesRows).toHaveCount(1) await expect(page.locator('td:has-text("nginx")').first()).toBeVisible() - const settingsButton = await page.waitForSelector(`[src="/instance_config_icon.svg"]:right-of(:text("nginx"))`) + const settingsButton = await page.waitForSelector(`[src="/concrete_container_config.svg"]:right-of(:text("nginx"))`) await settingsButton.click() await page.waitForURL(`${TEAM_ROUTES.deployment.list()}/**/instances/**`) await page.waitForSelector('h2:text-is("Container")') @@ -301,7 +301,7 @@ test.describe("Deleting copied deployment's parent", () => { const copyButton = page.locator('button:has-text("Copy")') await copyButton.click() - await page.locator(`button:has-text("${DAGENT_NODE}")`).click() + await page.locator(`button:has-text("${DAGENT_NODE}")`).first().click() await fillDeploymentPrefix(page, `${prefix}-other`) const currentUrl = page.url() @@ -329,7 +329,7 @@ test.describe("Deleting copied deployment's parent", () => { await expect(page.locator('td:has-text("nginx")').first()).toBeVisible() const newDeploymentSettingsButton = await page.waitForSelector( - `[src="/instance_config_icon.svg"]:right-of(:text("nginx"))`, + `[src="/concrete_container_config.svg"]:right-of(:text("nginx"))`, ) await newDeploymentSettingsButton.click() diff --git a/web/crux-ui/i18n.json b/web/crux-ui/i18n.json index a258c3f158..6bb23b694e 100644 --- a/web/crux-ui/i18n.json +++ b/web/crux-ui/i18n.json @@ -20,7 +20,6 @@ "/[teamSlug]/projects": ["projects"], "/[teamSlug]/projects/[projectId]": ["projects", "versions", "images", "deployments"], "/[teamSlug]/projects/[projectId]/versions/[versionId]": ["versions", "images", "deployments"], - "/[teamSlug]/projects/[projectId]/versions/[versionId]/images/[imageId]": ["images", "container"], "/[teamSlug]/nodes": ["nodes", "tokens"], "/[teamSlug]/nodes/[nodeId]": ["nodes", "images", "tokens", "deployments"], "/[teamSlug]/nodes/[nodeId]/log": [], @@ -38,13 +37,13 @@ "/[teamSlug]/deployments/[deploymentId]": ["images", "deployments", "nodes", "tokens", "container"], "/[teamSlug]/deployments/[deploymentId]/deploy": ["deployments"], "/[teamSlug]/deployments/[deploymentId]/log": [], - "/[teamSlug]/deployments/[deploymentId]/instances/[instanceId]": ["images", "deployments", "container"], "/status": ["status"], "/templates": ["templates", "projects"], "/composer": ["compose", "versions", "container"], "/[teamSlug]/dashboard": ["dashboard"], "/[teamSlug]/storages": ["storages"], "/[teamSlug]/storages/[storageId]": ["storages"], + "/[teamSlug]/container-configurations/[configId]": ["container"], "/[teamSlug]/config-bundles": ["config-bundles"], "/[teamSlug]/config-bundles/[configBundleId]": ["config-bundles"], "/[teamSlug]/packages": ["packages"], diff --git a/web/crux-ui/locales/en/common.json b/web/crux-ui/locales/en/common.json index d609c92d21..841215e310 100644 --- a/web/crux-ui/locales/en/common.json +++ b/web/crux-ui/locales/en/common.json @@ -250,5 +250,6 @@ }, "imageConfig": "Image config", - "instanceConfig": "Container config" + "instanceConfig": "Instance config", + "deploymentConfig": "Deployment config" } diff --git a/web/crux-ui/locales/en/container.json b/web/crux-ui/locales/en/container.json index bdfa3952fc..5a5c23c8ed 100644 --- a/web/crux-ui/locales/en/container.json +++ b/web/crux-ui/locales/en/container.json @@ -1,4 +1,8 @@ { + "containerConfigName": "Container Configs - {{name}}", + "editor": "Editor", + "json": "JSON", + "sensitiveKey": "Sensitive config data should be stored as secrets", "base": { @@ -150,5 +154,10 @@ "pathOverlapsSomePortranges": "{{path}} overlaps some port ranges", "missingExternalPort": "{{path}} is missing the external port definition", "shouldStartWithSlash": "{{path}} should start with a slash" + }, + + "errors": { + "ambiguousInConfigs": "Ambigous in {{configs}}", + "ambiguousKeyInConfigs": "Ambigous {{key}} in {{configs}}" } } diff --git a/web/crux-ui/locales/en/deployments.json b/web/crux-ui/locales/en/deployments.json index 7aa3040bf5..c624ea893c 100644 --- a/web/crux-ui/locales/en/deployments.json +++ b/web/crux-ui/locales/en/deployments.json @@ -24,6 +24,5 @@ "areYouSureDeployNodePrefix": "Are you sure you want start the deployment to {{node}} with the {{prefix}} prefix?", "noInstancesSelected": "No instances selected to deploy.", "protected": "Protected", - "configBundle": "Config bundle", - "bundleNameVariableWillBeOverwritten": "Bundle {{configBundle}} variable will be overwritten." + "configBundle": "Config bundle" } diff --git a/web/crux-ui/locales/en/images.json b/web/crux-ui/locales/en/images.json index eb72e6cb47..50e9d715e0 100644 --- a/web/crux-ui/locales/en/images.json +++ b/web/crux-ui/locales/en/images.json @@ -6,8 +6,6 @@ "imageNameAndTag": "Image name and tag", "containerName": "Container name", "tag": "Tag", - "json": "JSON", - "editor": "Editor", "filterMinChars": "Minimum filter length is {{length}} character", "availableTags": "Available tags", "environment": "Environment", diff --git a/web/crux-ui/package.json b/web/crux-ui/package.json index 2ae23b4549..83f8ca63d4 100644 --- a/web/crux-ui/package.json +++ b/web/crux-ui/package.json @@ -1,6 +1,6 @@ { "name": "crux-ui", - "version": "0.14.1", + "version": "0.15.0-rc", "description": "Open-source delivery platform that helps developers to deliver applications efficiently by simplifying software releases and operations in any environment.", "author": "dyrector.io", "private": true, diff --git a/web/crux-ui/playwright.config.ts b/web/crux-ui/playwright.config.ts index 3a26f30d37..136121d0f7 100644 --- a/web/crux-ui/playwright.config.ts +++ b/web/crux-ui/playwright.config.ts @@ -93,8 +93,8 @@ const config: PlaywrightTestConfig = { createProject('template', 'with-login/template.spec.ts'), createProject('project', 'with-login/project.spec.ts'), createProject('version', 'with-login/version.spec.ts'), - createProject('image-config', /with-login\/image-config\/(.*)/, ['registry', 'template', 'version']), - createProject('deployment', /with-login\/deployment(.*)\.spec\.ts/, ['image-config', 'nodes']), + createProject('container-config', /with-login\/container-config\/(.*)/, ['registry', 'template', 'version']), + createProject('deployment', /with-login\/deployment(.*)\.spec\.ts/, ['container-config', 'nodes']), createProject('dagent-deploy', 'with-login/nodes-deploy.spec.ts', ['deployment']), createProject('resource-copy', 'with-login/resource-copy.spec.ts', ['template', 'version', 'deployment', 'nodes']), createProject('dashboard', 'with-login/dashboard.spec.ts'), diff --git a/web/crux-ui/public/instance_config_icon.svg b/web/crux-ui/public/concrete_container_config.svg similarity index 100% rename from web/crux-ui/public/instance_config_icon.svg rename to web/crux-ui/public/concrete_container_config.svg diff --git a/web/crux-ui/public/concrete_container_config_turquoise.svg b/web/crux-ui/public/concrete_container_config_turquoise.svg new file mode 100644 index 0000000000..c079c359e3 --- /dev/null +++ b/web/crux-ui/public/concrete_container_config_turquoise.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/crux-ui/public/image_config_icon.svg b/web/crux-ui/public/container_config.svg similarity index 100% rename from web/crux-ui/public/image_config_icon.svg rename to web/crux-ui/public/container_config.svg diff --git a/web/crux-ui/public/container_config_turquoise.svg b/web/crux-ui/public/container_config_turquoise.svg new file mode 100644 index 0000000000..00a19a2ac8 --- /dev/null +++ b/web/crux-ui/public/container_config_turquoise.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/crux-ui/src/components/composer/converted-container.tsx b/web/crux-ui/src/components/composer/converted-container.tsx index cf37397e1e..8c9fd60e61 100644 --- a/web/crux-ui/src/components/composer/converted-container.tsx +++ b/web/crux-ui/src/components/composer/converted-container.tsx @@ -2,12 +2,12 @@ import { DyoCard } from '@app/elements/dyo-card' import { DyoHeading } from '@app/elements/dyo-heading' import DyoIcon from '@app/elements/dyo-icon' import DyoIndicator from '@app/elements/dyo-indicator' -import { ConvertedContainer, imageConfigToJsonContainerConfig } from '@app/models' +import { ConvertedContainer, containerConfigToJsonConfig } from '@app/models' import { writeToClipboard } from '@app/utils' import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' import { OFFLINE_EDITOR_STATE } from '../editor/use-item-editor-state' -import EditImageJson from '../projects/versions/images/edit-image-json' +import ContainerConfigJsonEditor from '../container-configs/container-config-json-editor' type ConvertedContainerCardProps = { className?: string @@ -44,13 +44,13 @@ const ConvertedContainerCard = (props: ConvertedContainerCardProps) => { {!hasRegistry && {t('missingRegistry')}}
- {}} onParseError={() => {}} - convertConfigToJson={imageConfigToJsonContainerConfig} + convertConfigToJson={containerConfigToJsonConfig} />
diff --git a/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx b/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx index a3abdc670e..ef67f8048c 100644 --- a/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx +++ b/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx @@ -1,4 +1,5 @@ -import { DyoCard, DyoCardProps } from '@app/elements/dyo-card' +import DyoButton from '@app/elements/dyo-button' +import { DyoCard } from '@app/elements/dyo-card' import DyoExpandableText from '@app/elements/dyo-expandable-text' import { DyoHeading } from '@app/elements/dyo-heading' import DyoLink from '@app/elements/dyo-link' @@ -8,7 +9,8 @@ import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' import Image from 'next/image' -interface ConfigBundleCardProps extends Omit { +type ConfigBundleCardProps = { + className?: string configBundle: ConfigBundle } @@ -37,6 +39,21 @@ const ConfigBundleCard = (props: ConfigBundleCardProps) => { buttonClassName="ml-auto" modalTitle={configBundle.name} /> + +
+ +
+ {t('common:config')} + {t('common:config')} +
+
+
) } diff --git a/web/crux-ui/src/components/config-bundles/config-bundle-page-menu.tsx b/web/crux-ui/src/components/config-bundles/config-bundle-page-menu.tsx deleted file mode 100644 index 138b0e4fe7..0000000000 --- a/web/crux-ui/src/components/config-bundles/config-bundle-page-menu.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import DyoButton from '@app/elements/dyo-button' -import { DyoConfirmationModal } from '@app/elements/dyo-modal' -import useConfirmation from '@app/hooks/use-confirmation' -import useTranslation from 'next-translate/useTranslation' -import { QA_DIALOG_LABEL_DELETE_CONFIG_BUNDLE } from 'quality-assurance' - -export type DetailsPageTexts = { - edit?: string - delete?: string - addDetailsItem?: string - discard?: string - save?: string -} - -export interface ConfigBundlePageMenuProps { - editing: boolean - deleteModalTitle: string - deleteModalDescription?: string - onDelete: VoidFunction - setEditing: (editing: boolean) => void -} - -export const ConfigBundlePageMenu = (props: ConfigBundlePageMenuProps) => { - const { editing, deleteModalTitle, deleteModalDescription, setEditing, onDelete } = props - - const { t } = useTranslation('common') - - const [deleteModalConfig, confirmDelete] = useConfirmation() - - const deleteClick = async () => { - const confirmed = await confirmDelete({ - qaLabel: QA_DIALOG_LABEL_DELETE_CONFIG_BUNDLE, - title: deleteModalTitle, - description: deleteModalDescription, - confirmText: t('delete'), - confirmColor: 'bg-error-red', - }) - - if (!confirmed) { - return - } - - onDelete() - } - - return editing ? ( - setEditing(false)}> - {t('back')} - - ) : ( - <> - setEditing(true)}> - {t('edit')} - - - {!onDelete ? null : ( - - {t('delete')} - - )} - - - - ) -} diff --git a/web/crux-ui/src/components/config-bundles/add-config-bundle-card.tsx b/web/crux-ui/src/components/config-bundles/edit-config-bundle-card.tsx similarity index 58% rename from web/crux-ui/src/components/config-bundles/add-config-bundle-card.tsx rename to web/crux-ui/src/components/config-bundles/edit-config-bundle-card.tsx index d5059204f9..692459d0aa 100644 --- a/web/crux-ui/src/components/config-bundles/add-config-bundle-card.tsx +++ b/web/crux-ui/src/components/config-bundles/edit-config-bundle-card.tsx @@ -3,58 +3,71 @@ import { DyoCard } from '@app/elements/dyo-card' import DyoForm from '@app/elements/dyo-form' import { DyoHeading } from '@app/elements/dyo-heading' import { DyoInput } from '@app/elements/dyo-input' +import { DyoLabel } from '@app/elements/dyo-label' import DyoTextArea from '@app/elements/dyo-text-area' import { defaultApiErrorHandler } from '@app/errors' import useDyoFormik from '@app/hooks/use-dyo-formik' import { SubmitHook } from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { ConfigBundle, CreateConfigBundle } from '@app/models' +import { ConfigBundleDetails, CreateConfigBundle, PatchConfigBundle } from '@app/models' import { sendForm } from '@app/utils' -import { configBundleCreateSchema } from '@app/validations' +import { configBundleSchema } from '@app/validations' import useTranslation from 'next-translate/useTranslation' +import { useState } from 'react' -interface AddConfigBundleCardProps { +type EditConfigBundleCardProps = { className?: string - configBundle?: ConfigBundle - onCreated: (configBundle: ConfigBundle) => void + configBundle?: ConfigBundleDetails + onConfigBundleEdited: (configBundle: ConfigBundleDetails) => void submit: SubmitHook } -const AddConfigBundleCard = (props: AddConfigBundleCardProps) => { - const { className, configBundle: propsConfigBundle, onCreated, submit } = props +const EditConfigBundleCard = (props: EditConfigBundleCardProps) => { + const { className, configBundle, onConfigBundleEdited, submit } = props const { t } = useTranslation('config-bundles') const routes = useTeamRoutes() + const [bundle, setBundle] = useState( + configBundle ?? { + id: null, + name: null, + description: null, + config: { + id: null, + type: 'config-bundle', + }, + }, + ) + + const editing = !!bundle.id + const handleApiError = defaultApiErrorHandler(t) const formik = useDyoFormik({ submit, - initialValues: { - name: propsConfigBundle?.name ?? '', - description: propsConfigBundle?.description ?? '', - }, - validationSchema: configBundleCreateSchema, + initialValues: bundle, + validationSchema: configBundleSchema, t, onSubmit: async (values, { setFieldError }) => { - const body: CreateConfigBundle = { + const body: CreateConfigBundle | PatchConfigBundle = { ...values, } - const res = await sendForm('POST', routes.configBundle.api.list(), body) + const res = await (!editing + ? sendForm('POST', routes.configBundle.api.list(), body) + : sendForm('PATCH', routes.configBundle.api.details(bundle.id), body)) if (res.ok) { - let result: ConfigBundle + let result: ConfigBundleDetails if (res.status !== 204) { - result = (await res.json()) as ConfigBundle + result = (await res.json()) as ConfigBundleDetails } else { - result = { - id: propsConfigBundle.id, - ...values, - } + result = values } - onCreated(result) + setBundle(result) + onConfigBundleEdited(result) } else { await handleApiError(res, setFieldError) } @@ -64,9 +77,11 @@ const AddConfigBundleCard = (props: AddConfigBundleCardProps) => { return ( - {t('new')} + {t(!editing ? 'new' : 'common:editName', configBundle)} + {t('tips')} +
{ ) } -export default AddConfigBundleCard +export default EditConfigBundleCard diff --git a/web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx b/web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx deleted file mode 100644 index 1b9cc8c9b2..0000000000 --- a/web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { CONFIG_BUNDLE_EDIT_WS_REQUEST_DELAY } from '@app/const' -import useTeamRoutes from '@app/hooks/use-team-routes' -import { useThrottling } from '@app/hooks/use-throttleing' -import useWebSocket from '@app/hooks/use-websocket' -import { - ConfigBundleDetails, - ConfigBundleUpdatedMessage, - PatchConfigBundleMessage, - WS_TYPE_CONFIG_BUNDLE_UPDATED, - WS_TYPE_PATCH_CONFIG_BUNDLE, - UniqueKeyValue, - WS_TYPE_CONFIG_BUNDLE_PATCH_RECEIVED, - WebSocketSaveState, -} from '@app/models' -import { useEffect, useState } from 'react' -import useEditorState from '../editor/use-editor-state' -import useItemEditorState, { ItemEditorState } from '../editor/use-item-editor-state' -import { toastWarning } from '@app/utils' -import { useRouter } from 'next/router' -import useTranslation from 'next-translate/useTranslation' -import { ValidationError } from 'yup' -import { getValidationError, configBundlePatchSchema } from '@app/validations' -import EditorBadge from '../editor/editor-badge' - -export type ConfigBundleStateOptions = { - configBundle: ConfigBundleDetails - onWsError: (error: Error) => void - onApiError: (error: Response) => void -} - -export type ConfigBundleState = { - configBundle: ConfigBundleDetails - editing: boolean - saveState: WebSocketSaveState - editorState: ItemEditorState - fieldErrors: ValidationError[] - topBarContent: React.ReactNode -} - -export type ConfigBundleActions = { - setEditing: (editing: boolean) => void - onDelete: () => Promise - onEditEnv: (envs: UniqueKeyValue[]) => void - onEditName: (name: string) => void - onEditDescription: (description: string) => void -} - -export const useConfigBundleDetailsState = ( - options: ConfigBundleStateOptions, -): [ConfigBundleState, ConfigBundleActions] => { - const { configBundle: propsConfigBundle, onWsError, onApiError } = options - - const { t } = useTranslation('config-bundles') - const router = useRouter() - const routes = useTeamRoutes() - - const throttle = useThrottling(CONFIG_BUNDLE_EDIT_WS_REQUEST_DELAY) - - const [configBundle, setConfigBundle] = useState(propsConfigBundle) - const [editing, setEditing] = useState(false) - const [saveState, setSaveState] = useState(null) - const [fieldErrors, setFieldErrors] = useState([]) - const [topBarContent, setTopBarContent] = useState(null) - - const sock = useWebSocket(routes.configBundle.detailsSocket(configBundle.id), { - onOpen: () => setSaveState('connected'), - onClose: () => setSaveState('disconnected'), - onSend: message => { - if (message.type === WS_TYPE_PATCH_CONFIG_BUNDLE) { - setSaveState('saving') - } - }, - onReceive: message => { - if (message.type === WS_TYPE_CONFIG_BUNDLE_PATCH_RECEIVED) { - setSaveState('saved') - } - }, - onError: onWsError, - }) - - const editor = useEditorState(sock) - const editorState = useItemEditorState(editor, sock, configBundle.id) - - sock.on(WS_TYPE_CONFIG_BUNDLE_UPDATED, (message: ConfigBundleUpdatedMessage) => { - setConfigBundle(it => ({ - ...it, - ...message, - })) - }) - - const onDelete = async (): Promise => { - const res = await fetch(routes.configBundle.api.details(configBundle.id), { - method: 'DELETE', - }) - - if (res.ok) { - await router.replace(routes.configBundle.list()) - } else if (res.status === 412) { - toastWarning(t('inUse')) - } else { - onApiError(res) - } - } - - const onPatch = (patch: Partial) => { - const newBundle = { - ...configBundle, - ...patch, - } - setConfigBundle(newBundle) - - const validationErrors = getValidationError(configBundlePatchSchema, newBundle, { abortEarly: false })?.inner ?? [] - setFieldErrors(validationErrors) - - if (validationErrors.length < 1) { - throttle(() => { - sock.send(WS_TYPE_PATCH_CONFIG_BUNDLE, { - name: newBundle.name, - description: newBundle.description, - environment: newBundle.environment, - } as PatchConfigBundleMessage) - }) - } - } - - const onEditEnv = (envs: UniqueKeyValue[]) => onPatch({ environment: envs }) - - const onEditName = (name: string) => onPatch({ name }) - - const onEditDescription = (description: string) => onPatch({ description }) - - useEffect(() => { - const reactNode = ( - <> - {editorState.editors.map((it, index) => ( - - ))} - - ) - - setTopBarContent(reactNode) - }, [editorState.editors]) - - return [ - { - configBundle, - editing, - saveState, - editorState, - fieldErrors, - topBarContent, - }, - { - setEditing, - onDelete, - onEditEnv, - onEditName, - onEditDescription, - }, - ] -} diff --git a/web/crux-ui/src/components/projects/versions/images/config/common-config-section.tsx b/web/crux-ui/src/components/container-configs/common-config-section.tsx similarity index 70% rename from web/crux-ui/src/components/projects/versions/images/config/common-config-section.tsx rename to web/crux-ui/src/components/container-configs/common-config-section.tsx index 5ce6ad7166..77bc0c7ae3 100644 --- a/web/crux-ui/src/components/projects/versions/images/config/common-config-section.tsx +++ b/web/crux-ui/src/components/container-configs/common-config-section.tsx @@ -11,25 +11,28 @@ import DyoMessage from '@app/elements/dyo-message' import DyoToggle from '@app/elements/dyo-toggle' import useTeamRoutes from '@app/hooks/use-team-routes' import { - COMMON_CONFIG_PROPERTIES, + COMMON_CONFIG_KEYS, CONTAINER_EXPOSE_STRATEGY_VALUES, CONTAINER_VOLUME_TYPE_VALUES, - CommonConfigDetails, - CommonConfigProperty, + CommonConfigKey, + ConcreteContainerConfigData, ContainerConfigData, + ContainerConfigErrors, ContainerConfigExposeStrategy, - ContainerConfigPort, - ContainerConfigVolume, - CraneConfigDetails, - ImageConfigProperty, + ContainerConfigKey, + ContainerConfigSectionType, InitContainerVolumeLink, - InstanceCommonConfigDetails, - InstanceCraneConfigDetails, + Port, StorageOption, + UniqueSecretKeyValue, + Volume, VolumeType, + booleanResettable, filterContains, filterEmpty, - mergeConfigs, + numberResettable, + portRangeToString, + stringResettable, } from '@app/models' import { fetcher, toNumber } from '@app/utils' import { @@ -47,34 +50,21 @@ import { v4 as uuid } from 'uuid' import ConfigSectionLabel from './config-section-label' import ExtendableItemList from './extendable-item-list' -type CommonConfigSectionBaseProps = { +type CommonConfigSectionProps = { disabled?: boolean - selectedFilters: ImageConfigProperty[] + selectedFilters: ContainerConfigKey[] editorOptions: ItemEditorState - config: T - resetableConfig?: T - onChange: (config: Partial) => void - onResetSection: (section: CommonConfigProperty) => void + resettableConfig: ContainerConfigData | ConcreteContainerConfigData + config: ContainerConfigData | ConcreteContainerConfigData + onChange: (config: ContainerConfigData | ConcreteContainerConfigData) => void + onResetSection: (section: CommonConfigKey) => void fieldErrors: ContainerConfigValidationErrors + conflictErrors: ContainerConfigErrors definedSecrets?: string[] publicKey?: string + baseConfig: ContainerConfigData | null } -type ImageCommonConfigSectionProps = CommonConfigSectionBaseProps< - CommonConfigDetails & Pick -> & { - configType: 'image' -} - -type InstanceCommonConfigSectionProps = CommonConfigSectionBaseProps< - InstanceCommonConfigDetails & Pick -> & { - configType: 'instance' - imageConfig: ContainerConfigData -} - -type CommonConfigSectionProps = ImageCommonConfigSectionProps | InstanceCommonConfigSectionProps - const CommonConfigSection = (props: CommonConfigSectionProps) => { const { t } = useTranslation('container') const routes = useTeamRoutes() @@ -86,24 +76,21 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { selectedFilters, editorOptions, fieldErrors, - configType, + conflictErrors, definedSecrets, publicKey, - config: propsConfig, - resetableConfig: propsResetableConfig, + resettableConfig, + config, + baseConfig, } = props const { data: storages } = useSWR(routes.storage.api.options(), fetcher) - const disabledOnImage = configType === 'image' || disabled - // eslint-disable-next-line react/destructuring-assignment - const imageConfig = configType === 'instance' ? props.imageConfig : null - const resetableConfig = propsResetableConfig ?? propsConfig - const config = configType === 'instance' ? mergeConfigs(imageConfig, propsConfig) : propsConfig + const sectionType: ContainerConfigSectionType = baseConfig ? 'concrete' : 'base' const exposedPorts = config.ports?.filter(it => !!it.internal) ?? [] - const onVolumesChanged = (it: ContainerConfigVolume[]) => + const onVolumesChanged = (it: Volume[]) => onChange({ volumes: it, storage: @@ -115,8 +102,8 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { : undefined, }) - const onPortsChanged = (ports: ContainerConfigPort[]) => { - let patch: Partial> = { + const onPortsChanged = (ports: Port[]) => { + let patch: ConcreteContainerConfigData = { ports, } @@ -147,7 +134,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { const environmentWarning = getValidationError(unsafeUniqueKeyValuesSchema, config.environment, undefined, t)?.message ?? null - return !filterEmpty([...COMMON_CONFIG_PROPERTIES], selectedFilters) ? null : ( + return !filterEmpty([...COMMON_CONFIG_KEYS], selectedFilters) ? null : (
{t('base.common').toUpperCase()} @@ -160,7 +147,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => {
onResetSection('name')} > {t('common.containerName').toUpperCase()} @@ -175,7 +162,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { placeholder={t('common.containerName')} onPatch={it => onChange({ name: it })} editorOptions={editorOptions} - message={findErrorFor(fieldErrors, 'name')} + message={findErrorFor(fieldErrors, 'name') ?? conflictErrors?.name} disabled={disabled} />
@@ -188,11 +175,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => {
onResetSection('user')} > {t('common.user').toUpperCase()} @@ -203,14 +186,14 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { containerClassName="max-w-lg mb-3" labelClassName="text-bright font-semibold tracking-wide mb-2 my-auto mr-4" grow - value={config.user === -1 ? '' : config.user} + value={config.user !== -1 ? config.user : ''} placeholder={t('common.placeholders.containerDefault')} onPatch={it => { const val = toNumber(it) - onChange({ user: configType === 'instance' || val === 0 ? val : val ?? -1 }) + onChange({ user: typeof val !== 'number' ? -1 : val }) }} editorOptions={editorOptions} - message={findErrorFor(fieldErrors, 'user')} + message={findErrorFor(fieldErrors, 'user') ?? conflictErrors?.user} disabled={disabled} />
@@ -222,7 +205,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => {
onResetSection('workingDirectory')} > {t('common.workingDirectory').toUpperCase()} @@ -237,7 +222,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { placeholder={t('common.placeholders.containerDefault')} onPatch={it => onChange({ workingDirectory: it })} editorOptions={editorOptions} - message={findErrorFor(fieldErrors, 'workingDirectory')} + message={findErrorFor(fieldErrors, 'workingDirectory') ?? conflictErrors?.workingDirectory} disabled={disabled} />
@@ -248,8 +233,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {filterContains('expose', selectedFilters) && (
onResetSection('expose')} + error={conflictErrors?.expose} > {t('common.exposeStrategy').toUpperCase()} @@ -271,8 +257,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {filterContains('tty', selectedFilters) && (
onResetSection('tty')} + error={conflictErrors?.tty} > {t('common.tty').toUpperCase()} @@ -291,8 +278,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {filterContains('configContainer', selectedFilters) && (
onResetSection('configContainer')} + error={conflictErrors?.configContainer} > {t('common.configContainer').toUpperCase()} @@ -357,8 +345,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {filterContains('routing', selectedFilters) && (
onResetSection('routing')} + error={conflictErrors?.routing} > {t('common.routing').toUpperCase()} @@ -458,13 +447,14 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { labelClassName="text-bright font-semibold tracking-wide mb-2" label={t('common.environment').toUpperCase()} onChange={it => onChange({ environment: it })} - onResetSection={resetableConfig.environment ? () => onResetSection('environment') : null} + onResetSection={resettableConfig.environment ? () => onResetSection('environment') : null} items={config.environment} editorOptions={editorOptions} disabled={disabled} findErrorMessage={index => findErrorStartsWith(fieldErrors, `environment[${index}]`)} message={findErrorFor(fieldErrors, `environment`) ?? environmentWarning} messageType={!findErrorFor(fieldErrors, `environment`) && environmentWarning ? 'info' : 'error'} + errors={conflictErrors?.environment} />
)} @@ -472,14 +462,14 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {/* secrets */} {filterContains('secrets', selectedFilters) && (
- {configType === 'instance' ? ( + {sectionType === 'concrete' ? ( onChange({ secrets: it })} - items={propsConfig.secrets} + items={config.secrets as UniqueSecretKeyValue[]} editorOptions={editorOptions} definedSecrets={definedSecrets} publicKey={publicKey} @@ -492,7 +482,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { labelClassName="text-bright font-semibold tracking-wide mb-2" label={t('common.secrets').toUpperCase()} onChange={it => onChange({ secrets: it.map(sit => ({ ...sit })) })} - onResetSection={resetableConfig.secrets ? () => onResetSection('secrets') : null} + onResetSection={resettableConfig.secrets ? () => onResetSection('secrets') : null} items={config.secrets} editorOptions={editorOptions} disabled={disabled} @@ -511,7 +501,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { label={t('common.commands').toUpperCase()} labelClassName="text-bright font-semibold tracking-wide mb-2" onChange={it => onChange({ commands: it })} - onResetSection={resetableConfig.commands ? () => onResetSection('commands') : null} + onResetSection={resettableConfig.commands ? () => onResetSection('commands') : null} items={config.commands} editorOptions={editorOptions} disabled={disabled} @@ -529,7 +519,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { label={t('common.arguments').toUpperCase()} labelClassName="text-bright font-semibold tracking-wide mb-2" onChange={it => onChange({ args: it })} - onResetSection={resetableConfig.args ? () => onResetSection('args') : null} + onResetSection={resettableConfig.args ? () => onResetSection('args') : null} items={config.args} editorOptions={editorOptions} disabled={disabled} @@ -543,8 +533,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {filterContains('storage', selectedFilters) && (
onResetSection('storage')} + error={conflictErrors?.storage} > {t('common.storage').toUpperCase()} @@ -580,7 +571,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { placeholder={t('common.bucketPath')} onPatch={it => onChange({ storage: { ...config.storage, bucket: it } })} editorOptions={editorOptions} - disabled={disabled || !config.storage?.storageId} + disabled={disabled || !resettableConfig.storage?.storageId} message={findErrorFor(fieldErrors, 'storage.bucket')} /> @@ -600,7 +591,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { storage: { ...config.storage, path: it }, }) } - disabled={disabled || !config.storage?.storageId} + disabled={disabled || !resettableConfig.storage?.storageId} />
{ items={config.ports} label={t('common.ports')} onPatch={it => onPortsChanged(it)} - onResetSection={resetableConfig.ports ? () => onResetSection('ports') : null} + onResetSection={resettableConfig.ports ? () => onResetSection('ports') : null} findErrorMessage={index => findErrorStartsWith(fieldErrors, `ports[${index}]`)} emptyItemFactory={() => ({ external: null, internal: null, })} renderItem={(item, error, removeButton, onPatch) => ( -
-
- { - const value = Number.parseInt(it, 10) - const external = Number.isNaN(value) ? null : value - - onPatch({ - external, - }) - }} - editorOptions={editorOptions} - disabled={disabled} - /> -
+ <> +
+
+ { + const value = Number.parseInt(it, 10) + const external = Number.isNaN(value) ? null : value + + onPatch({ + external, + }) + }} + editorOptions={editorOptions} + disabled={disabled} + /> +
-
- - onPatch({ - internal: toNumber(it), - }) - } - editorOptions={editorOptions} - disabled={disabled} - /> +
+ + onPatch({ + internal: toNumber(it), + }) + } + editorOptions={editorOptions} + disabled={disabled} + /> +
+ + {removeButton()}
- {removeButton()} -
+ {(conflictErrors?.ports ?? {})[item.internal] && ( + + )} + )} /> )} @@ -688,72 +685,81 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { })} findErrorMessage={index => findErrorStartsWith(fieldErrors, `portRanges[${index}]`)} onPatch={it => onChange({ portRanges: it })} - onResetSection={resetableConfig.portRanges ? () => onResetSection('portRanges') : null} + onResetSection={resettableConfig.portRanges ? () => onResetSection('portRanges') : null} renderItem={(item, error, removeButton, onPatch) => ( -
- {t('common.internal').toUpperCase()} - -
- onPatch({ ...item, internal: { ...item.internal, from: toNumber(it) } })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'internal.from')} - /> + <> +
+ {t('common.internal').toUpperCase()} + +
+ onPatch({ ...item, internal: { ...item.internal, from: toNumber(it) } })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'internal.from')} + /> - onPatch({ ...item, internal: { ...item.internal, to: toNumber(it) } })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'internal.to')} - /> -
+ onPatch({ ...item, internal: { ...item.internal, to: toNumber(it) } })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'internal.to')} + /> +
- {/* external part */} - {t('common.external').toUpperCase()} - -
- onPatch({ ...item, external: { ...item.external, from: toNumber(it) } })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'external.from')} - /> + {/* external part */} + {t('common.external').toUpperCase()} + +
+ onPatch({ ...item, external: { ...item.external, from: toNumber(it) } })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'external.from')} + /> - onPatch({ ...item, external: { ...item.external, to: toNumber(it) } })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'external.from')} - /> + onPatch({ ...item, external: { ...item.external, to: toNumber(it) } })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'external.from')} + /> - {removeButton()} + {removeButton()} +
-
+ + {(conflictErrors?.portRanges ?? {})[portRangeToString(item.internal)] && ( + + )} + )} /> )} @@ -772,89 +778,95 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { })} findErrorMessage={index => findErrorStartsWith(fieldErrors, `volumes[${index}]`)} onPatch={it => onVolumesChanged(it)} - onResetSection={resetableConfig.volumes ? () => onResetSection('volumes') : null} + onResetSection={resettableConfig.volumes ? () => onResetSection('volumes') : null} renderItem={(item, error, removeButton, onPatch) => ( -
-
- onPatch({ name: it })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'name')} - /> + <> +
+
+ onPatch({ name: it })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'name')} + /> - onPatch({ size: it })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'size')} - /> -
+ onPatch({ size: it })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'size')} + /> +
-
- onPatch({ path: it })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'path')} - /> +
+ onPatch({ path: it })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'path')} + /> - onPatch({ class: it })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'class')} - /> -
+ onPatch({ class: it })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'class')} + /> +
-
-
- {t('common.type')} - -
- t(`common.volumeTypes.${it}`)} - onSelectionChange={it => onPatch({ type: it })} - disabled={disabled} - qaLabel={chipsQALabelFromValue} - /> - - {removeButton('self-center ml-auto')} +
+
+ {t('common.type')} + +
+ t(`common.volumeTypes.${it}`)} + onSelectionChange={it => onPatch({ type: it })} + disabled={disabled} + qaLabel={chipsQALabelFromValue} + /> + + {removeButton('self-center ml-auto')} +
-
+ + {(conflictErrors?.volumes ?? {})[item.path] && ( + + )} + )} /> )} @@ -865,6 +877,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { disabled={disabled} items={config.initContainers} label={t('common.initContainers')} + error={conflictErrors?.initContainers} emptyItemFactory={() => ({ id: uuid(), name: null, @@ -877,7 +890,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { })} findErrorMessage={index => findErrorStartsWith(fieldErrors, `initContainers[${index}]`)} onPatch={it => onChange({ initContainers: it })} - onResetSection={resetableConfig.initContainers ? () => onResetSection('initContainers') : null} + onResetSection={resettableConfig.initContainers ? () => onResetSection('initContainers') : null} renderItem={(item, error, removeButton, onPatch) => (
{ + const { disabled, className, labelClassName, children, onResetSection, error } = props + + return ( +
+
+ {children} + + {!disabled && ( +
+ +
+ )} +
+ + {error && } +
+ ) +} + +export default ConfigSectionLabel diff --git a/web/crux-ui/src/components/projects/versions/images/config/config-to-filters.ts b/web/crux-ui/src/components/container-configs/config-to-filters.ts similarity index 71% rename from web/crux-ui/src/components/projects/versions/images/config/config-to-filters.ts rename to web/crux-ui/src/components/container-configs/config-to-filters.ts index 5dbbba9852..6734ce529d 100644 --- a/web/crux-ui/src/components/projects/versions/images/config/config-to-filters.ts +++ b/web/crux-ui/src/components/container-configs/config-to-filters.ts @@ -1,12 +1,12 @@ -import { ALL_CONFIG_PROPERTIES, ContainerConfigData, ImageConfigProperty } from '@app/models' +import { CONTAINER_CONFIG_KEYS, ContainerConfigData, ContainerConfigKey } from '@app/models' import { ContainerConfigValidationErrors, findErrorStartsWith } from '@app/validations' const configToFilters = ( - current: ImageConfigProperty[], + current: ContainerConfigKey[], configData: T, fieldErrors?: ContainerConfigValidationErrors, -): ImageConfigProperty[] => { - const newFilters = ALL_CONFIG_PROPERTIES.filter(it => { +): ContainerConfigKey[] => { + const newFilters = CONTAINER_CONFIG_KEYS.filter(it => { const value = configData[it] if (fieldErrors && findErrorStartsWith(fieldErrors, it)) { @@ -21,10 +21,6 @@ const configToFilters = ( return false } - if (Array.isArray(value) && value.length < 1) { - return false - } - if (typeof value === 'object') { return Object.keys(value).length > 0 } diff --git a/web/crux-ui/src/components/projects/versions/images/image-config-filters.tsx b/web/crux-ui/src/components/container-configs/container-config-filters.tsx similarity index 71% rename from web/crux-ui/src/components/projects/versions/images/image-config-filters.tsx rename to web/crux-ui/src/components/container-configs/container-config-filters.tsx index 4ebea6c70f..9bc52e6093 100644 --- a/web/crux-ui/src/components/projects/versions/images/image-config-filters.tsx +++ b/web/crux-ui/src/components/container-configs/container-config-filters.tsx @@ -1,39 +1,39 @@ import { DyoLabel } from '@app/elements/dyo-label' import { - ALL_CONFIG_PROPERTIES, - BaseImageConfigFilterType, - COMMON_CONFIG_PROPERTIES, - CRANE_CONFIG_PROPERTIES, - DAGENT_CONFIG_PROPERTIES, - ImageConfigProperty, + ContainerConfigFilterType, + COMMON_CONFIG_KEYS, + CONTAINER_CONFIG_KEYS, + ContainerConfigKey, + CRANE_CONFIG_KEYS, + DAGENT_CONFIG_KEYS, } from '@app/models' import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' -type FilterSet = Record +type FilterSet = Record export const defaultFilterSet: FilterSet = { - all: [...ALL_CONFIG_PROPERTIES], - common: [...COMMON_CONFIG_PROPERTIES], - crane: [...CRANE_CONFIG_PROPERTIES].filter(it => it !== 'extraLBAnnotations'), - dagent: [...DAGENT_CONFIG_PROPERTIES], + all: [...CONTAINER_CONFIG_KEYS], + common: [...COMMON_CONFIG_KEYS], + crane: [...CRANE_CONFIG_KEYS].filter(it => it !== 'extraLBAnnotations'), + dagent: [...DAGENT_CONFIG_KEYS], } export const k8sFilterSet: FilterSet = { - all: [...COMMON_CONFIG_PROPERTIES, ...CRANE_CONFIG_PROPERTIES], - common: [...COMMON_CONFIG_PROPERTIES], - crane: [...CRANE_CONFIG_PROPERTIES].filter(it => it !== 'extraLBAnnotations'), + all: [...COMMON_CONFIG_KEYS, ...CRANE_CONFIG_KEYS], + common: [...COMMON_CONFIG_KEYS], + crane: [...CRANE_CONFIG_KEYS].filter(it => it !== 'extraLBAnnotations'), dagent: null, } export const dockerFilterSet: FilterSet = { - all: [...COMMON_CONFIG_PROPERTIES, ...DAGENT_CONFIG_PROPERTIES], - common: [...COMMON_CONFIG_PROPERTIES], + all: [...COMMON_CONFIG_KEYS, ...DAGENT_CONFIG_KEYS], + common: [...COMMON_CONFIG_KEYS], crane: null, - dagent: [...DAGENT_CONFIG_PROPERTIES], + dagent: [...DAGENT_CONFIG_KEYS], } -const getBorderColor = (type: BaseImageConfigFilterType): string => { +const getBorderColor = (type: ContainerConfigFilterType): string => { switch (type) { case 'common': return 'border-dyo-orange/50' @@ -46,7 +46,7 @@ const getBorderColor = (type: BaseImageConfigFilterType): string => { } } -const getBgColor = (type: BaseImageConfigFilterType): string => { +const getBgColor = (type: ContainerConfigFilterType): string => { switch (type) { case 'common': return 'bg-dyo-orange/50' @@ -59,22 +59,22 @@ const getBgColor = (type: BaseImageConfigFilterType): string => { } } -type ImageConfigFilterProps = { - onChange: (filters: ImageConfigProperty[]) => void - filters: ImageConfigProperty[] +type ContainerConfigFilterProps = { + onChange: (filters: ContainerConfigKey[]) => void + filters: ContainerConfigKey[] filterSet?: FilterSet } -const ImageConfigFilters = (props: ImageConfigFilterProps) => { +const ContainerConfigFilters = (props: ContainerConfigFilterProps) => { const { onChange, filters, filterSet = defaultFilterSet } = props const filterSetKeys = Object.entries(filterSet) .filter(([_, value]) => !!value) - .map(([key]) => key) as BaseImageConfigFilterType[] + .map(([key]) => key) as ContainerConfigFilterType[] const { t } = useTranslation('container') - const onBaseFilterChanged = (value: BaseImageConfigFilterType) => { + const onBaseFilterChanged = (value: ContainerConfigFilterType) => { const baseFilters = filterSet[value] const select = filters.filter(it => baseFilters.includes(it)).length === baseFilters.length @@ -85,7 +85,7 @@ const ImageConfigFilters = (props: ImageConfigFilterProps) => { } } - const onFilterChanged = (value: ImageConfigProperty) => { + const onFilterChanged = (value: ContainerConfigKey) => { const newFilters = filters.indexOf(value) !== -1 ? filters.filter(it => it !== value) : [...filters, value] onChange(newFilters) } @@ -98,7 +98,7 @@ const ImageConfigFilters = (props: ImageConfigFilterProps) => { {filterSetKeys.map((base, index) => { const selected = base === 'all' - ? filters.length === ALL_CONFIG_PROPERTIES.length + ? filters.length === CONTAINER_CONFIG_KEYS.length : filters.filter(it => filterSet[base].includes(it)).length === filterSet[base].length return ( @@ -146,4 +146,4 @@ const ImageConfigFilters = (props: ImageConfigFilterProps) => { ) } -export default ImageConfigFilters +export default ContainerConfigFilters diff --git a/web/crux-ui/src/components/projects/versions/images/edit-image-json.tsx b/web/crux-ui/src/components/container-configs/container-config-json-editor.tsx similarity index 83% rename from web/crux-ui/src/components/projects/versions/images/edit-image-json.tsx rename to web/crux-ui/src/components/container-configs/container-config-json-editor.tsx index 7480af8247..8f29ac24df 100644 --- a/web/crux-ui/src/components/projects/versions/images/edit-image-json.tsx +++ b/web/crux-ui/src/components/container-configs/container-config-json-editor.tsx @@ -1,13 +1,11 @@ import { ItemEditorState } from '@app/components/editor/use-item-editor-state' import useMultiInputState from '@app/components/editor/use-multi-input-state' import JsonEditor from '@app/components/shared/json-editor-dynamic-module' -import { IMAGE_WS_REQUEST_DELAY } from '@app/const' -import { CANCEL_THROTTLE, useThrottling } from '@app/hooks/use-throttleing' import { editIdOf } from '@app/models' import clsx from 'clsx' import { CSSProperties, useCallback, useState } from 'react' -interface EditImageJsonProps { +type EditImageJsonProps = { disabled?: boolean className?: string config: Config @@ -19,7 +17,7 @@ interface EditImageJsonProps { const JSON_EDITOR_COMPARATOR = (one: Json, other: Json): boolean => JSON.stringify(one) === JSON.stringify(other) -const EditImageJson = (props: EditImageJsonProps) => { +const ContainerConfigJsonEditor = (props: EditImageJsonProps) => { const { disabled, editorOptions, @@ -30,8 +28,6 @@ const EditImageJson = (props: EditImageJsonProps) => convertConfigToJson, } = props - const throttle = useThrottling(IMAGE_WS_REQUEST_DELAY) - const [jsonError, setJsonError] = useState(false) const onMergeValues = (remote: Json, local: Json): Json => { @@ -64,12 +60,9 @@ const EditImageJson = (props: EditImageJsonProps) => const onParseError = useCallback( (err: Error) => { setJsonError(true) - - throttle(CANCEL_THROTTLE) - propOnParseError(err) }, - [throttle, propOnParseError], + [propOnParseError], ) const { highlightColor } = editorState @@ -96,4 +89,4 @@ const EditImageJson = (props: EditImageJsonProps) => ) } -export default EditImageJson +export default ContainerConfigJsonEditor diff --git a/web/crux-ui/src/components/projects/versions/images/config/crane-config-section.tsx b/web/crux-ui/src/components/container-configs/crane-config-section.tsx similarity index 90% rename from web/crux-ui/src/components/projects/versions/images/config/crane-config-section.tsx rename to web/crux-ui/src/components/container-configs/crane-config-section.tsx index 5c32427df6..47972190ff 100644 --- a/web/crux-ui/src/components/projects/versions/images/config/crane-config-section.tsx +++ b/web/crux-ui/src/components/container-configs/crane-config-section.tsx @@ -5,76 +5,57 @@ import KeyValueInput from '@app/components/shared/key-value-input' import DyoChips, { chipsQALabelFromValue } from '@app/elements/dyo-chips' import { DyoHeading } from '@app/elements/dyo-heading' import { DyoLabel } from '@app/elements/dyo-label' +import DyoMessage from '@app/elements/dyo-message' import DyoToggle from '@app/elements/dyo-toggle' import { - CRANE_CONFIG_FILTER_VALUES, - CraneConfigProperty, - ImageConfigProperty, - filterContains, - filterEmpty, CONTAINER_DEPLOYMENT_STRATEGY_VALUES, - CommonConfigDetails, + CRANE_CONFIG_FILTER_VALUES, + ConcreteContainerConfigData, ContainerConfigData, + ContainerConfigErrors, + ContainerConfigKey, ContainerDeploymentStrategyType, - CraneConfigDetails, - InstanceContainerConfigData, - InstanceCraneConfigDetails, - mergeConfigs, + CraneConfigKey, + booleanResettable, + filterContains, + filterEmpty, + stringResettable, } from '@app/models' import { nullify, toNumber } from '@app/utils' +import { ContainerConfigValidationErrors, findErrorFor } from '@app/validations' import useTranslation from 'next-translate/useTranslation' -import ConfigSectionLabel from './config-section-label' -import DyoMessage from '@app/elements/dyo-message' import { useEffect } from 'react' -import { ContainerConfigValidationErrors, findErrorFor } from '@app/validations' +import ConfigSectionLabel from './config-section-label' -type CraneConfigSectionBaseProps = { - config: T - resetableConfig?: T - onChange: (config: Partial) => void - onResetSection: (section: CraneConfigProperty) => void - selectedFilters: ImageConfigProperty[] +type CraneConfigSectionProps = { + config: ContainerConfigData | ConcreteContainerConfigData + onChange: (config: ContainerConfigData | ConcreteContainerConfigData) => void + onResetSection: (section: CraneConfigKey) => void + selectedFilters: ContainerConfigKey[] editorOptions: ItemEditorState disabled?: boolean fieldErrors: ContainerConfigValidationErrors + conflictErrors: ContainerConfigErrors + baseConfig: ContainerConfigData | null + resettableConfig: ContainerConfigData | ConcreteContainerConfigData } -type ImageCraneConfigSectionProps = CraneConfigSectionBaseProps< - CraneConfigDetails & Pick -> & { - configType: 'image' -} - -type InstanceCraneConfigSectionProps = CraneConfigSectionBaseProps< - InstanceCraneConfigDetails & Pick -> & { - configType: 'instance' - imageConfig: ContainerConfigData -} - -export type CraneConfigSectionProps = ImageCraneConfigSectionProps | InstanceCraneConfigSectionProps - const CraneConfigSection = (props: CraneConfigSectionProps) => { const { - config: propsConfig, - resetableConfig: propsResetableConfig, - configType, + config, + resettableConfig, + baseConfig, selectedFilters, onChange, onResetSection, editorOptions, disabled, fieldErrors, + conflictErrors, } = props const { t } = useTranslation('container') - const disabledOnImage = configType === 'image' || disabled - // eslint-disable-next-line react/destructuring-assignment - const imageConfig = configType === 'instance' ? props.imageConfig : null - const resetableConfig = propsResetableConfig ?? propsConfig - const config = configType === 'instance' ? mergeConfigs(imageConfig, propsConfig) : propsConfig - const ports = config.ports?.filter(it => !!it.internal) ?? [] useEffect(() => { @@ -99,8 +80,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('deploymentStrategy', selectedFilters) && (
onResetSection('deploymentStrategy')} + error={conflictErrors?.deploymentStrategy} > {t('crane.deploymentStrategy').toUpperCase()} @@ -122,8 +104,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('healthCheckConfig', selectedFilters) && (
onResetSection('healthCheckConfig')} + error={conflictErrors?.healthCheckConfig} > {t('crane.healthCheckConfig').toUpperCase()} @@ -223,7 +206,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { onChange={it => onChange({ customHeaders: it })} editorOptions={editorOptions} disabled={disabled} - onResetSection={resetableConfig.customHeaders ? () => onResetSection('customHeaders') : null} + onResetSection={resettableConfig.customHeaders ? () => onResetSection('customHeaders') : null} />
@@ -233,8 +216,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('resourceConfig', selectedFilters) && (
onResetSection('resourceConfig')} + error={conflictErrors?.resourceConfig} > {t('crane.resourceConfig').toUpperCase()} @@ -343,8 +327,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('proxyHeaders', selectedFilters) && (
onResetSection('proxyHeaders')} + error={conflictErrors?.proxyHeaders} > {t('crane.proxyHeaders').toUpperCase()} @@ -364,8 +349,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => {
onResetSection('useLoadBalancer')} + error={conflictErrors?.useLoadBalancer} > {t('crane.useLoadBalancer').toUpperCase()} @@ -387,7 +373,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.extraLBAnnotations ?? []} editorOptions={editorOptions} onChange={it => onChange({ extraLBAnnotations: it })} - onResetSection={resetableConfig.extraLBAnnotations ? () => onResetSection('extraLBAnnotations') : null} + onResetSection={resettableConfig.extraLBAnnotations ? () => onResetSection('extraLBAnnotations') : null} disabled={disabled} /> )} @@ -397,10 +383,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {/* Labels */} {filterContains('labels', selectedFilters) && (
- onResetSection('labels')} - > + onResetSection('labels')}> {t('crane.labels').toUpperCase()} @@ -413,6 +396,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.labels?.deployment ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.labels?.deployment} />
@@ -426,6 +410,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.labels?.service ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.labels?.service} />
@@ -439,6 +424,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.labels?.ingress ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.labels?.ingress} />
@@ -449,7 +435,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('annotations', selectedFilters) && (
onResetSection('annotations')} > {t('crane.annotations').toUpperCase()} @@ -464,6 +450,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.annotations?.deployment ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.annotations?.deployment} />
@@ -477,6 +464,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.annotations?.service ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.annotations.service} />
@@ -490,6 +478,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.annotations?.ingress ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.annotations.ingress} />
@@ -500,8 +489,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('metrics', selectedFilters) && (
onResetSection('metrics')} + error={conflictErrors?.metrics} > {t('crane.metrics').toUpperCase()} diff --git a/web/crux-ui/src/components/projects/versions/images/config/dagent-config-section.tsx b/web/crux-ui/src/components/container-configs/dagent-config-section.tsx similarity index 83% rename from web/crux-ui/src/components/projects/versions/images/config/dagent-config-section.tsx rename to web/crux-ui/src/components/container-configs/dagent-config-section.tsx index d7d717ab51..971c19ea32 100644 --- a/web/crux-ui/src/components/projects/versions/images/config/dagent-config-section.tsx +++ b/web/crux-ui/src/components/container-configs/dagent-config-section.tsx @@ -1,79 +1,65 @@ +import MultiInput from '@app/components/editor/multi-input' import { ItemEditorState } from '@app/components/editor/use-item-editor-state' import KeyOnlyInput from '@app/components/shared/key-only-input' import KeyValueInput from '@app/components/shared/key-value-input' import DyoChips, { chipsQALabelFromValue } from '@app/elements/dyo-chips' import { DyoHeading } from '@app/elements/dyo-heading' import { DyoLabel } from '@app/elements/dyo-label' +import DyoMessage from '@app/elements/dyo-message' import { - DagentConfigProperty, - DAGENT_CONFIG_PROPERTIES, - filterContains, - filterEmpty, - ImageConfigProperty, + ConcreteContainerConfigData, + CONTAINER_LOG_DRIVER_VALUES, + CONTAINER_NETWORK_MODE_VALUES, + CONTAINER_RESTART_POLICY_TYPE_VALUES, + CONTAINER_STATE_VALUES, ContainerConfigData, + ContainerConfigErrors, + ContainerConfigKey, ContainerLogDriverType, ContainerNetworkMode, ContainerRestartPolicyType, - CONTAINER_LOG_DRIVER_VALUES, - CONTAINER_NETWORK_MODE_VALUES, - CONTAINER_RESTART_POLICY_TYPE_VALUES, - DagentConfigDetails, - InstanceDagentConfigDetails, - mergeConfigs, ContainerState, - CONTAINER_STATE_VALUES, + DAGENT_CONFIG_KEYS, + DagentConfigKey, + filterContains, + filterEmpty, + stringResettable, } from '@app/models' +import { toNumber } from '@app/utils' +import { ContainerConfigValidationErrors, findErrorFor } from '@app/validations' import useTranslation from 'next-translate/useTranslation' import ConfigSectionLabel from './config-section-label' -import DyoMessage from '@app/elements/dyo-message' -import { ContainerConfigValidationErrors, findErrorFor } from '@app/validations' -import MultiInput from '@app/components/editor/multi-input' -import { toNumber } from '@app/utils' -type DagentConfigSectionBaseProps = { - config: T - resetableConfig?: T - onChange: (config: Partial) => void - onResetSection: (section: DagentConfigProperty) => void - selectedFilters: ImageConfigProperty[] +type DagentConfigSectionProps = { + config: ContainerConfigData | ConcreteContainerConfigData + onChange: (config: ContainerConfigData | ConcreteContainerConfigData) => void + onResetSection: (section: DagentConfigKey) => void + selectedFilters: ContainerConfigKey[] editorOptions: ItemEditorState disabled?: boolean fieldErrors: ContainerConfigValidationErrors + conflictErrors: ContainerConfigErrors + baseConfig: ContainerConfigData | null + resettableConfig: ContainerConfigData | ConcreteContainerConfigData } -type ImageDagentConfigSectionProps = DagentConfigSectionBaseProps & { - configType: 'image' -} - -type InstanceDagentConfigSectionProps = DagentConfigSectionBaseProps & { - configType: 'instance' - imageConfig: ContainerConfigData -} - -type DagentConfigSectionProps = ImageDagentConfigSectionProps | InstanceDagentConfigSectionProps - const DagentConfigSection = (props: DagentConfigSectionProps) => { const { - config: propsConfig, - resetableConfig: propsResetableConfig, - configType, + config, + resettableConfig, + baseConfig, onResetSection, selectedFilters, onChange, editorOptions, disabled, fieldErrors, + conflictErrors, } = props const { t } = useTranslation('container') - const disabledOnImage = configType === 'image' || disabled - // eslint-disable-next-line react/destructuring-assignment - const imageConfig = configType === 'instance' ? props.imageConfig : null - const resetableConfig = propsResetableConfig ?? propsConfig - const config = configType === 'instance' ? mergeConfigs(imageConfig, propsConfig) : propsConfig - - return !filterEmpty([...DAGENT_CONFIG_PROPERTIES], selectedFilters) ? null : ( + return !filterEmpty([...DAGENT_CONFIG_KEYS], selectedFilters) ? null : (
{t('base.dagent')} @@ -84,8 +70,9 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { {filterContains('networkMode', selectedFilters) && (
onResetSection('networkMode')} + error={conflictErrors?.networkMode} > {t('dagent.networkMode').toUpperCase()} @@ -112,7 +99,7 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { items={config.networks ?? []} keyPlaceholder={t('dagent.placeholders.network')} onChange={it => onChange({ networks: it })} - onResetSection={resetableConfig.networks ? () => onResetSection('networks') : null} + onResetSection={resettableConfig.networks ? () => onResetSection('networks') : null} unique={false} editorOptions={editorOptions} disabled={disabled} @@ -129,10 +116,11 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { labelClassName="text-bright font-semibold tracking-wide mb-2" label={t('dagent.dockerLabels').toUpperCase()} onChange={it => onChange({ dockerLabels: it })} - onResetSection={resetableConfig.dockerLabels ? () => onResetSection('dockerLabels') : null} + onResetSection={resettableConfig.dockerLabels ? () => onResetSection('dockerLabels') : null} items={config.dockerLabels ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.dockerLabels} />
@@ -142,8 +130,9 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { {filterContains('restartPolicy', selectedFilters) && (
onResetSection('restartPolicy')} + error={conflictErrors?.restartPolicy} > {t('dagent.restartPolicy').toUpperCase()} @@ -165,8 +154,9 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { {filterContains('logConfig', selectedFilters) && (
onResetSection('logConfig')} + error={conflictErrors?.logConfig} > {t('dagent.logConfig').toUpperCase()} @@ -203,8 +193,9 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { {filterContains('expectedState', selectedFilters) && (
onResetSection('expectedState')} + error={conflictErrors?.expectedState} > {t('dagent.expectedState').toUpperCase()} diff --git a/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx b/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx new file mode 100644 index 0000000000..3564fde5ee --- /dev/null +++ b/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx @@ -0,0 +1,116 @@ +import EditContainerConfigHeading from '@app/components/container-configs/edit-container-config-heading' +import useContainerConfigSocket from '@app/components/container-configs/use-container-config-socket' +import useContainerConfigState, { setParseError } from '@app/components/container-configs/use-container-config-state' +import usePatchContainerConfig from '@app/components/container-configs/use-patch-container-config' +import useEditorState from '@app/components/editor/use-editor-state' +import useItemEditorState from '@app/components/editor/use-item-editor-state' +import { DyoCard } from '@app/elements/dyo-card' +import DyoImgButton from '@app/elements/dyo-img-button' +import DyoMessage from '@app/elements/dyo-message' +import { DyoConfirmationModal } from '@app/elements/dyo-modal' +import useConfirmation from '@app/hooks/use-confirmation' +import { ContainerConfig, ContainerConfigParent, VersionImage } from '@app/models' +import { createContainerConfigSchema, getValidationError } from '@app/validations' +import useTranslation from 'next-translate/useTranslation' +import { QA_DIALOG_LABEL_DELETE_IMAGE } from 'quality-assurance' +import ContainerConfigJsonEditor from './container-config-json-editor' + +type EditContainerCardProps = { + configParent: ContainerConfigParent + containerConfig: Config + image?: VersionImage + onDelete?: VoidFunction + convertConfigToJson: (config: Config) => Json + mergeJsonWithConfig: (config: Config, json: Json) => Config +} + +const EditContainerConfigCard = (props: EditContainerCardProps) => { + const { configParent, containerConfig, image, onDelete, convertConfigToJson, mergeJsonWithConfig } = props + const disabled = !configParent.mutable + + const { t } = useTranslation('images') + + const [state, dispatch] = useContainerConfigState({ + config: containerConfig, + parseError: null, + saveState: 'disconnected', + }) + const sock = useContainerConfigSocket(containerConfig.id, dispatch) + const [wsPatchConfig, cancelPatch] = usePatchContainerConfig(sock, dispatch) + const [deleteModal, confirmDelete] = useConfirmation() + + const name = containerConfig.name ?? configParent.name + + const deleteImage = async () => { + const confirmed = await confirmDelete({ + qaLabel: QA_DIALOG_LABEL_DELETE_IMAGE, + title: t('common:areYouSureDeleteName', { name }), + description: t('common:proceedYouLoseAllDataToName', { name }), + confirmText: t('common:delete'), + confirmColor: 'bg-error-red', + }) + + if (!confirmed) { + return + } + + onDelete() + } + + const editor = useEditorState(sock) + const editorState = useItemEditorState(editor, sock, containerConfig.id) + + const onPatch = (patch: Json) => { + const config = mergeJsonWithConfig(state.config as Config, patch) + wsPatchConfig({ + config, + }) + } + + const onParseError = (error: Error) => { + cancelPatch() + dispatch(setParseError(error.message)) + } + + const errorMessage = + state.parseError ?? + getValidationError(createContainerConfigSchema(image?.labels ?? {}), state.config, null, t)?.message + + return ( + <> + +
+ + + {!disabled && onDelete && ( + + )} +
+ + {errorMessage ? ( + + ) : null} + +
+ +
+
+ + + + ) +} + +export default EditContainerConfigCard diff --git a/web/crux-ui/src/components/projects/versions/images/edit-image-heading.tsx b/web/crux-ui/src/components/container-configs/edit-container-config-heading.tsx similarity index 82% rename from web/crux-ui/src/components/projects/versions/images/edit-image-heading.tsx rename to web/crux-ui/src/components/container-configs/edit-container-config-heading.tsx index 1ab59f6917..7c7313233a 100644 --- a/web/crux-ui/src/components/projects/versions/images/edit-image-heading.tsx +++ b/web/crux-ui/src/components/container-configs/edit-container-config-heading.tsx @@ -2,14 +2,14 @@ import { DyoHeading } from '@app/elements/dyo-heading' import { DyoLabel } from '@app/elements/dyo-label' import useTranslation from 'next-translate/useTranslation' -interface EditImageHeadingProps { +type EditContainerConfigHeadingProps = { className?: string imageName: string - imageTag: string + imageTag: string | null containerName: string } -const EditImageHeading = (props: EditImageHeadingProps) => { +const EditContainerConfigHeading = (props: EditContainerConfigHeadingProps) => { const { imageName, imageTag, containerName, className } = props const { t } = useTranslation('common') @@ -36,4 +36,4 @@ const EditImageHeading = (props: EditImageHeadingProps) => { ) } -export default EditImageHeading +export default EditContainerConfigHeading diff --git a/web/crux-ui/src/components/projects/versions/images/config/extendable-item-list.tsx b/web/crux-ui/src/components/container-configs/extendable-item-list.tsx similarity index 97% rename from web/crux-ui/src/components/projects/versions/images/config/extendable-item-list.tsx rename to web/crux-ui/src/components/container-configs/extendable-item-list.tsx index e49530091d..be707c6e37 100644 --- a/web/crux-ui/src/components/projects/versions/images/config/extendable-item-list.tsx +++ b/web/crux-ui/src/components/container-configs/extendable-item-list.tsx @@ -1,11 +1,11 @@ import DyoImgButton from '@app/elements/dyo-img-button' import DyoMessage from '@app/elements/dyo-message' import useRepatch, { RepatchAction } from '@app/hooks/use-repatch' +import { ErrorWithPath } from '@app/validations' import clsx from 'clsx' import { useEffect } from 'react' import { v4 as uuid } from 'uuid' import ConfigSectionLabel from './config-section-label' -import { ErrorWithPath } from '@app/validations' const addItem = (item: Omit): RepatchAction> => @@ -92,11 +92,13 @@ type InternalState = { editedItemId?: string } -interface ExtendableItemListProps { +type ExtendableItemListProps = { itemClassName?: string disabled?: boolean label: string + error?: string items: T[] + // id to error message renderItem: ( item: T, error: ErrorWithPath, @@ -120,6 +122,7 @@ const ExtendableItemList = (props: ExtendableItemListProps) = onResetSection, emptyItemFactory, itemClassName, + error: sectionLabelError, } = props const [state, dispatch] = useRepatch>({ @@ -150,6 +153,7 @@ const ExtendableItemList = (props: ExtendableItemListProps) = labelClassName="text-bright font-semibold tracking-wide" disabled={!hasValue || disabled || !onResetSection} onResetSection={onResetSection} + error={sectionLabelError} > {label.toUpperCase()} diff --git a/web/crux-ui/src/components/container-configs/use-container-config-socket.ts b/web/crux-ui/src/components/container-configs/use-container-config-socket.ts new file mode 100644 index 0000000000..9501373662 --- /dev/null +++ b/web/crux-ui/src/components/container-configs/use-container-config-socket.ts @@ -0,0 +1,38 @@ +import useTeamRoutes from '@app/hooks/use-team-routes' +import useWebSocket from '@app/hooks/use-websocket' +import { ConfigUpdatedMessage, WS_TYPE_CONFIG_UPDATED, WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_RECEIVED } from '@app/models' +import WebSocketClientEndpoint from '@app/websockets/websocket-client-endpoint' +import { useCallback } from 'react' +import { ContainerConfigDispatch, patchConfig, setSaveState } from './use-container-config-state' + +const useContainerConfigSocket = (configId: string, dispatch: ContainerConfigDispatch): WebSocketClientEndpoint => { + const routes = useTeamRoutes() + + const sock = useWebSocket(routes.containerConfig.detailsSocket(configId), { + onOpen: () => dispatch(setSaveState('connected')), + onClose: () => dispatch(setSaveState('disconnected')), + onSend: message => { + if (message.type === WS_TYPE_PATCH_CONFIG) { + dispatch(setSaveState('saving')) + } + }, + onReceive: message => { + if (message.type === WS_TYPE_PATCH_RECEIVED) { + dispatch(setSaveState('saved')) + } + }, + }) + + const onConfigUpdated = useCallback( + (config: ConfigUpdatedMessage) => { + dispatch(patchConfig(config)) + }, + [dispatch], + ) + + sock.on(WS_TYPE_CONFIG_UPDATED, onConfigUpdated) + + return sock +} + +export default useContainerConfigSocket diff --git a/web/crux-ui/src/components/container-configs/use-container-config-state.ts b/web/crux-ui/src/components/container-configs/use-container-config-state.ts new file mode 100644 index 0000000000..a1809bce81 --- /dev/null +++ b/web/crux-ui/src/components/container-configs/use-container-config-state.ts @@ -0,0 +1,68 @@ +import useRepatch, { RepatchAction } from '@app/hooks/use-repatch' +import { + ConcreteContainerConfig, + ContainerConfig, + ContainerConfigData, + ContainerConfigKey, + WebSocketSaveState, +} from '@app/models' +import { Dispatch } from 'react' + +// state +export type ContainerConfigState = { + config: ContainerConfig | ConcreteContainerConfig + saveState: WebSocketSaveState + parseError: string +} + +export type ContainerConfigAction = RepatchAction +export type ContainerConfigDispatch = Dispatch + +// actions +export const setSaveState = + (saveState: WebSocketSaveState): ContainerConfigAction => + state => ({ + ...state, + saveState, + }) + +export const setParseError = + (error: string): ContainerConfigAction => + state => ({ + ...state, + parseError: error, + }) + +export const patchConfig = + (config: ContainerConfigData): ContainerConfigAction => + state => ({ + ...state, + saveState: 'saving', + config: { + ...state.config, + ...config, + }, + }) + +export const resetSection = + (section: ContainerConfigKey): ContainerConfigAction => + state => { + const newConfg: ContainerConfig = { + ...state.config, + } + + newConfg[section as string] = null + return { + ...state, + saveState: 'saving', + config: newConfg, + } + } + +// selectors + +// hook +const useContainerConfigState = (initialState: ContainerConfigState): [ContainerConfigState, ContainerConfigDispatch] => + useRepatch(initialState) + +export default useContainerConfigState diff --git a/web/crux-ui/src/components/container-configs/use-patch-container-config.ts b/web/crux-ui/src/components/container-configs/use-patch-container-config.ts new file mode 100644 index 0000000000..7e93d50b35 --- /dev/null +++ b/web/crux-ui/src/components/container-configs/use-patch-container-config.ts @@ -0,0 +1,54 @@ +import { WS_PATCH_DELAY } from '@app/const' +import { CANCEL_THROTTLE, useThrottling } from '@app/hooks/use-throttleing' +import { ContainerConfigData, PatchConfigMessage, WS_TYPE_PATCH_CONFIG } from '@app/models' +import WebSocketClientEndpoint from '@app/websockets/websocket-client-endpoint' +import { useCallback, useRef } from 'react' +import { ContainerConfigDispatch, patchConfig, resetSection } from './use-container-config-state' + +type PatchConfig = (config: PatchConfigMessage) => void + +const usePatchContainerConfig = ( + sock: WebSocketClientEndpoint, + dispatch: ContainerConfigDispatch, +): [PatchConfig, VoidFunction] => { + const configPatches = useRef({}) + + const throttle = useThrottling(WS_PATCH_DELAY) + + const wsSend = sock.send + + const patchCallback = useCallback( + (patch: PatchConfigMessage) => { + if (patch.resetSection) { + dispatch(resetSection(patch.resetSection)) + wsSend(WS_TYPE_PATCH_CONFIG, patch) + return + } + + configPatches.current = { + ...configPatches.current, + ...patch.config, + } + + dispatch(patchConfig(patch.config)) + + throttle(() => { + const wsPatch: PatchConfigMessage = { + config: configPatches.current, + } + + wsSend(WS_TYPE_PATCH_CONFIG, wsPatch) + }) + }, + [wsSend, throttle, dispatch], + ) + + const cancelThrottle = useCallback(() => { + throttle(CANCEL_THROTTLE) + configPatches.current = {} + }, [throttle]) + + return [patchCallback, cancelThrottle] +} + +export default usePatchContainerConfig diff --git a/web/crux-ui/src/components/dashboard/onboarding.tsx b/web/crux-ui/src/components/dashboard/onboarding.tsx index 50af662ee2..a5507b9523 100644 --- a/web/crux-ui/src/components/dashboard/onboarding.tsx +++ b/web/crux-ui/src/components/dashboard/onboarding.tsx @@ -47,7 +47,7 @@ const onboardingLinkFactories = (routes: TeamRoutes): Record { diff --git a/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx b/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx index 6665339cd8..6121ec5b9f 100644 --- a/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx +++ b/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx @@ -32,8 +32,8 @@ interface DeploymentContainerStatusListProps { progress: Record } -type ContainerWithInstance = Container & { - instanceId: string +type ContainerWithConfigId = Container & { + configId: string } const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps) => { @@ -44,12 +44,12 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps const now = utcNow() - const [containers, setContainers] = useState(() => + const [containers, setContainers] = useState(() => deployment.instances.map(it => ({ - instanceId: it.id, + configId: it.config.id, id: { prefix: deployment.prefix, - name: it.config?.name ?? it.image.config.name, + name: it.config.name ?? it.image.config.name, }, createdAt: null, state: null, @@ -73,7 +73,7 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps } as WatchContainerStatusMessage), }) - const merge = (weak: ContainerWithInstance[], strong: Container[]): ContainerWithInstance[] => { + const merge = (weak: ContainerWithConfigId[], strong: Container[]): ContainerWithConfigId[] => { if (!strong || strong.length === 0) { return weak } @@ -88,7 +88,7 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps ? it : { ...strong[index], - instanceId: it.instanceId, + configId: it.configId, } }) } @@ -109,7 +109,7 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps } return !containers ? null : ( - + ( @@ -121,9 +121,9 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps /> - progress[it.instanceId]?.progress < 1 ? ( - + body={(it: ContainerWithConfigId) => + progress[it.configId]?.progress < 1 ? ( + ) : ( {`${it.imageName}:${it.imageTag}`} ) @@ -136,7 +136,7 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps /> ( + body={(it: ContainerWithConfigId) => ( <> {it.state && (
@@ -149,11 +149,8 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps
)} - - + + )} diff --git a/web/crux-ui/src/components/deployments/deployment-details-section.tsx b/web/crux-ui/src/components/deployments/deployment-details-section.tsx index 5b2fa4111e..c1d2cefd9d 100644 --- a/web/crux-ui/src/components/deployments/deployment-details-section.tsx +++ b/web/crux-ui/src/components/deployments/deployment-details-section.tsx @@ -1,42 +1,25 @@ -import useItemEditorState from '@app/components/editor/use-item-editor-state' -import KeyValueInput from '@app/components/shared/key-value-input' +import DyoButton from '@app/elements/dyo-button' import { DyoCard } from '@app/elements/dyo-card' -import DyoIcon from '@app/elements/dyo-icon' import { DyoLabel } from '@app/elements/dyo-label' -import DyoLink from '@app/elements/dyo-link' -import DyoMultiSelect from '@app/elements/dyo-multi-select' import useTeamRoutes from '@app/hooks/use-team-routes' -import { ConfigBundleOption } from '@app/models' -import { auditToLocaleDate, fetcher } from '@app/utils' +import { auditToLocaleDate } from '@app/utils' import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' -import useSWR from 'swr' +import Image from 'next/image' import DeploymentStatusTag from './deployment-status-tag' -import { DeploymentActions, DeploymentState } from './use-deployment-state' +import { DeploymentState } from './use-deployment-state' -interface DeploymentDetailsSectionProps { +type DeploymentDetailsSectionProps = { className?: string state: DeploymentState - actions: DeploymentActions } -const ITEM_ID = 'deployment' - const DeploymentDetailsSection = (props: DeploymentDetailsSectionProps) => { - const { state, actions, className } = props - const { deployment, mutable, editor, sock } = state + const { state, className } = props + const { deployment } = state const { t } = useTranslation('deployments') - const teamRoutes = useTeamRoutes() - - const editorState = useItemEditorState(editor, sock, ITEM_ID) - - const { data: configBundleOptions } = useSWR(teamRoutes.configBundle.api.options(), fetcher) - - const configBundlesHref = - deployment.configBundleIds?.length === 1 - ? teamRoutes.configBundle.details(deployment.configBundleIds[0]) - : teamRoutes.configBundle.list() + const routes = useTeamRoutes() return ( @@ -50,42 +33,20 @@ const DeploymentDetailsSection = (props: DeploymentDetailsSectionProps) => {
- - {t('configBundle').toUpperCase()} - - -
- it.id} - labelConverter={it => it.name} - selection={deployment.configBundleIds ?? []} - onSelectionChange={it => actions.onConfigBundlesSelected(it)} - /> - - - - +
+ +
+ {t('common:config')} + {t('common:config')} +
+
- - - deployment.configBundleEnvironment[key] - ? t('bundleNameVariableWillBeOverwritten', { - configBundle: deployment.configBundleEnvironment[key], - }) - : null - } - /> ) } diff --git a/web/crux-ui/src/components/deployments/deployment-view-list.tsx b/web/crux-ui/src/components/deployments/deployment-view-list.tsx index 94ce2606d9..9bf91fe5b2 100644 --- a/web/crux-ui/src/components/deployments/deployment-view-list.tsx +++ b/web/crux-ui/src/components/deployments/deployment-view-list.tsx @@ -3,7 +3,7 @@ import DyoIcon from '@app/elements/dyo-icon' import DyoLink from '@app/elements/dyo-link' import DyoTable, { DyoColumn, dyoCheckboxColumn, sortDate, sortString } from '@app/elements/dyo-table' import useTeamRoutes from '@app/hooks/use-team-routes' -import { Instance } from '@app/models' +import { Instance, containerNameOfInstance } from '@app/models' import { utcDateToLocale } from '@app/utils' import useTranslation from 'next-translate/useTranslation' import { DeploymentActions, DeploymentState } from './use-deployment-state' @@ -18,7 +18,7 @@ const DeploymentViewList = (props: DeploymentViewListProps) => { const routes = useTeamRoutes() const { state, actions } = props - const { instances, deployInstances, project, version, deployment } = state + const { instances, deployInstances } = state return ( @@ -34,9 +34,9 @@ const DeploymentViewList = (props: DeploymentViewListProps) => { header={t('containerName')} className="w-4/12" sortable - sortField={(it: Instance) => it.config?.name ?? it.image.config.name} + sortField={containerNameOfInstance} sort={sortString} - body={(it: Instance) => it.config?.name ?? it.image.config.name} + body={containerNameOfInstance} /> { <>
- +
- + )} diff --git a/web/crux-ui/src/components/deployments/deployment-view-tile.tsx b/web/crux-ui/src/components/deployments/deployment-view-tile.tsx index d9246abedf..6f6fdc28bc 100644 --- a/web/crux-ui/src/components/deployments/deployment-view-tile.tsx +++ b/web/crux-ui/src/components/deployments/deployment-view-tile.tsx @@ -1,20 +1,39 @@ import DyoWrap from '@app/elements/dyo-wrap' -import EditInstanceCard from './instances/edit-instance-card' -import { DeploymentActions, DeploymentState } from './use-deployment-state' +import { + concreteContainerConfigToJsonConfig, + ContainerConfigParent, + mergeJsonConfigToConcreteContainerConfig, +} from '@app/models' +import EditContainerConfigCard from '../container-configs/edit-container-config-card' +import { DeploymentState } from './use-deployment-state' -export interface DeploymentViewTileProps { +export type DeploymentViewTileProps = { state: DeploymentState - actions: DeploymentActions } const DeploymentViewTile = (props: DeploymentViewTileProps) => { - const { state, actions } = props + const { state } = props return ( - {state.instances.map(it => ( - - ))} + {state.instances.map(it => { + const parent: ContainerConfigParent = { + id: it.image.id, + name: it.image.name, + mutable: state.mutable, + } + + return ( + + ) + })} ) } diff --git a/web/crux-ui/src/components/deployments/edit-deployment-card.tsx b/web/crux-ui/src/components/deployments/edit-deployment-card.tsx index 5cf93e3cf5..93f0eaf791 100644 --- a/web/crux-ui/src/components/deployments/edit-deployment-card.tsx +++ b/web/crux-ui/src/components/deployments/edit-deployment-card.tsx @@ -2,20 +2,28 @@ import DyoButton from '@app/elements/dyo-button' import { DyoCard } from '@app/elements/dyo-card' import DyoForm from '@app/elements/dyo-form' import { DyoHeading } from '@app/elements/dyo-heading' +import DyoIcon from '@app/elements/dyo-icon' import { DyoInput } from '@app/elements/dyo-input' +import { DyoLabel } from '@app/elements/dyo-label' +import DyoLink from '@app/elements/dyo-link' +import DyoMultiSelect from '@app/elements/dyo-multi-select' import DyoTextArea from '@app/elements/dyo-text-area' import DyoToggle from '@app/elements/dyo-toggle' import { defaultApiErrorHandler } from '@app/errors' import useDyoFormik from '@app/hooks/use-dyo-formik' import { SubmitHook } from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { DeploymentDetails, PatchDeployment } from '@app/models' -import { sendForm } from '@app/utils' +import { ConfigBundle, Deployment, DeploymentDetails, detailsToConfigBundle, UpdateDeployment } from '@app/models' +import { fetcher, sendForm } from '@app/utils' import { updateDeploymentSchema } from '@app/validations' import useTranslation from 'next-translate/useTranslation' -import React from 'react' +import useSWR from 'swr' -interface EditDeploymentCardProps { +type EditableDeployment = Pick & { + configBundles: ConfigBundle[] +} + +type EditDeploymentCardProps = { className?: string deployment: DeploymentDetails submit: SubmitHook @@ -23,38 +31,54 @@ interface EditDeploymentCardProps { } const EditDeploymentCard = (props: EditDeploymentCardProps) => { - const { deployment, className, onDeploymentEdited, submit } = props + const { deployment: propsDeployment, className, onDeploymentEdited, submit } = props + + const deployment: EditableDeployment = { + ...propsDeployment, + configBundles: propsDeployment.configBundles.map(it => detailsToConfigBundle(it)), + } const { t } = useTranslation('deployments') const routes = useTeamRoutes() const handleApiError = defaultApiErrorHandler(t) + const { data: configBundles, error: configBundlesError } = useSWR( + routes.configBundle.api.list(), + fetcher, + ) + const formik = useDyoFormik({ submit, initialValues: deployment, validationSchema: updateDeploymentSchema, t, onSubmit: async (values, { setFieldError }) => { - const transformedValues = updateDeploymentSchema.cast(values) as any - - const body: PatchDeployment = { - ...transformedValues, + const body: UpdateDeployment = { + ...values, + configBundles: values.configBundles.map(it => it.id), } - const res = await sendForm('PATCH', routes.deployment.api.details(deployment.id), body) + let res = await sendForm('PUT', routes.deployment.api.details(deployment.id), body) if (res.ok) { - onDeploymentEdited({ - ...deployment, - ...transformedValues, - }) - } else { - await handleApiError(res, setFieldError) + res = await fetch(routes.deployment.api.details(deployment.id)) + if (res.ok) { + const deploy: DeploymentDetails = await res.json() + + onDeploymentEdited(deploy) + return + } } + await handleApiError(res, setFieldError) }, }) + const configBundlesHref = + deployment.configBundles?.length === 1 + ? routes.configBundle.details(deployment.configBundles[0].id) + : routes.configBundle.list() + return ( @@ -90,6 +114,26 @@ const EditDeploymentCard = (props: EditDeploymentCardProps) => { value={formik.values.note} /> + {t('configBundle')} + + {!configBundlesError && ( +
+ { + await formik.setFieldValue('configBundles', it) + }} + /> + + + + +
+ )} +
diff --git a/web/crux-ui/src/components/deployments/edit-deployment-instances.tsx b/web/crux-ui/src/components/deployments/edit-deployment-instances.tsx index 878523990c..b124a4db5f 100644 --- a/web/crux-ui/src/components/deployments/edit-deployment-instances.tsx +++ b/web/crux-ui/src/components/deployments/edit-deployment-instances.tsx @@ -19,7 +19,7 @@ const EditDeploymentInstances = (props: EditDeploymentInstancesProps) => {
{viewMode === 'tile' ? ( - + ) : ( )} diff --git a/web/crux-ui/src/components/deployments/instances/edit-instance-card.tsx b/web/crux-ui/src/components/deployments/instances/edit-instance-card.tsx deleted file mode 100644 index 8d7aade05e..0000000000 --- a/web/crux-ui/src/components/deployments/instances/edit-instance-card.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import useItemEditorState from '@app/components/editor/use-item-editor-state' -import { DyoCard } from '@app/elements/dyo-card' -import DyoMessage from '@app/elements/dyo-message' -import { - Instance, - instanceConfigToJsonInstanceConfig, - InstanceJsonContainerConfig, - mergeJsonConfigToInstanceContainerConfig, -} from '@app/models' -import EditImageHeading from '../../projects/versions/images/edit-image-heading' -import EditImageJson from '../../projects/versions/images/edit-image-json' -import { DeploymentActions, DeploymentState } from '../use-deployment-state' -import useInstanceState from './use-instance-state' - -interface EditInstanceCardProps { - instance: Instance - deploymentState: DeploymentState - deploymentActions: DeploymentActions -} - -const EditInstanceCard = (props: EditInstanceCardProps) => { - const { instance, deploymentState, deploymentActions } = props - const { editor, sock } = deploymentState - - const [state, actions] = useInstanceState({ - instance, - deploymentState, - deploymentActions, - }) - - const { config, errorMessage } = state - - const editorState = useItemEditorState(editor, sock, instance.id) - - return ( - -
- -
- - {errorMessage ? ( - - ) : null} - -
- - actions.onPatch(instance.id, mergeJsonConfigToInstanceContainerConfig(config, it)) - } - onParseError={actions.onParseError} - convertConfigToJson={instanceConfigToJsonInstanceConfig} - /> -
-
- ) -} - -export default EditInstanceCard diff --git a/web/crux-ui/src/components/deployments/instances/use-instance-state.ts b/web/crux-ui/src/components/deployments/instances/use-instance-state.ts deleted file mode 100644 index b05223fac6..0000000000 --- a/web/crux-ui/src/components/deployments/instances/use-instance-state.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - ContainerConfigData, - GetInstanceSecretsMessage, - ImageConfigProperty, - Instance, - InstanceContainerConfigData, - InstanceSecretsMessage, - mergeConfigs, - MergedContainerConfigData, - PatchInstanceMessage, - WS_TYPE_GET_INSTANCE_SECRETS, - WS_TYPE_INSTANCE_SECRETS, - WS_TYPE_PATCH_INSTANCE, -} from '@app/models' -import { createContainerConfigSchema, getValidationError } from '@app/validations' -import { useEffect, useState } from 'react' -import { DeploymentActions, DeploymentState } from '../use-deployment-state' -import useTranslation from 'next-translate/useTranslation' - -export type InstanceStateOptions = { - deploymentState: DeploymentState - deploymentActions: DeploymentActions - instance: Instance -} - -export type InstanceState = { - config: MergedContainerConfigData - resetableConfig: ContainerConfigData - definedSecrets: string[] - errorMessage: string -} - -export type InstanceActions = { - resetSection: (section: ImageConfigProperty) => void - onPatch: (newConfig: Partial) => void - onParseError: (error: Error) => void -} - -const useInstanceState = (options: InstanceStateOptions) => { - const { t } = useTranslation('container') - - const { instance, deploymentState, deploymentActions } = options - const { sock } = deploymentState - - const [parseError, setParseError] = useState(null) - const [definedSecrets, setDefinedSecrets] = useState([]) - - sock.on(WS_TYPE_INSTANCE_SECRETS, (message: InstanceSecretsMessage) => { - if (message.instanceId !== instance.id) { - return - } - - setDefinedSecrets(message.keys) - }) - - useEffect(() => { - sock.send(WS_TYPE_GET_INSTANCE_SECRETS, { - id: instance.id, - } as GetInstanceSecretsMessage) - }, [instance.id, sock]) - - const mergedConfig = mergeConfigs(instance.image.config, instance.config) - - const errorMessage = - parseError ?? getValidationError(createContainerConfigSchema(instance.image.labels), mergedConfig, null, t)?.message - - const resetSection = (section: ImageConfigProperty): InstanceContainerConfigData => { - const newConfig = { ...instance.config } as any - newConfig[section] = null - - deploymentActions.updateInstanceConfig(instance.id, newConfig) - - sock.send(WS_TYPE_PATCH_INSTANCE, { - instanceId: instance.id, - resetSection: section, - } as PatchInstanceMessage) - - return newConfig - } - - const onPatch = (id: string, newConfig: InstanceContainerConfigData) => { - deploymentActions.onPatchInstance(id, newConfig) - setParseError(null) - } - - const onParseError = (err: Error) => setParseError(err.message) - - return [ - { - config: mergedConfig, - resetableConfig: instance.config, - definedSecrets, - errorMessage, - }, - { - onPatch, - resetSection, - onParseError, - }, - ] -} - -export default useInstanceState diff --git a/web/crux-ui/src/components/deployments/use-deployment-state.tsx b/web/crux-ui/src/components/deployments/use-deployment-state.tsx index 225b0e8a2a..e1e4885cf4 100644 --- a/web/crux-ui/src/components/deployments/use-deployment-state.tsx +++ b/web/crux-ui/src/components/deployments/use-deployment-state.tsx @@ -1,17 +1,13 @@ import useEditorState, { EditorState } from '@app/components/editor/use-editor-state' import useNodeState from '@app/components/nodes/use-node-state' import { ViewMode } from '@app/components/shared/view-mode-toggle' -import { DEPLOYMENT_EDIT_WS_REQUEST_DELAY } from '@app/const' import { DyoConfirmationModalConfig } from '@app/elements/dyo-modal' import useConfirmation from '@app/hooks/use-confirmation' import usePersistedViewMode from '@app/hooks/use-persisted-view-mode' import useTeamRoutes from '@app/hooks/use-team-routes' -import { useThrottling } from '@app/hooks/use-throttleing' import useWebSocket from '@app/hooks/use-websocket' import { DeploymentDetails, - DeploymentEnvUpdatedMessage, - DeploymentInvalidatedSecrets, deploymentIsCopiable, deploymentIsDeletable, deploymentIsDeployable, @@ -20,34 +16,22 @@ import { DeploymentRoot, DeploymentToken, DyoNode, - GetInstanceMessage, ImageDeletedMessage, Instance, - InstanceContainerConfigData, - InstanceMessage, + instanceCreatedMessageToInstance, InstancesAddedMessage, - InstanceUpdatedMessage, NodeEventMessage, - PatchInstanceMessage, ProjectDetails, - UniqueKeyValue, VersionDetails, WebSocketSaveState, - WS_TYPE_DEPLOYMENT_ENV_UPDATED, - WS_TYPE_GET_INSTANCE, WS_TYPE_IMAGE_DELETED, - WS_TYPE_INSTANCE, WS_TYPE_INSTANCES_ADDED, - WS_TYPE_INSTANCE_UPDATED, WS_TYPE_NODE_EVENT, - WS_TYPE_PATCH_DEPLOYMENT_ENV, - WS_TYPE_PATCH_INSTANCE, - WS_TYPE_PATCH_RECEIVED, } from '@app/models' import WebSocketClientEndpoint from '@app/websockets/websocket-client-endpoint' import useTranslation from 'next-translate/useTranslation' import { QA_DIALOG_LABEL_REVOKE_DEPLOY_TOKEN } from 'quality-assurance' -import { useRef, useState } from 'react' +import { useState } from 'react' export type DeploymentEditState = 'details' | 'edit' | 'copy' | 'create-token' @@ -80,26 +64,13 @@ export type DeploymentState = { export type DeploymentActions = { setEditState: (state: DeploymentEditState) => void onDeploymentEdited: (editedDeployment: DeploymentDetails) => void - onEnvironmentEdited: (environment: UniqueKeyValue[]) => void - onPatchInstance: (id: string, newConfig: InstanceContainerConfigData) => void - updateInstanceConfig: (id: string, newConfig: InstanceContainerConfigData) => void setViewMode: (viewMode: ViewMode) => void - onInvalidateSecrets: (secrets: DeploymentInvalidatedSecrets[]) => void onDeploymentTokenCreated: (token: DeploymentToken) => void onRevokeDeploymentToken: VoidFunction onInstanceSelected: (id: string, deploy: boolean) => void onAllInstancesToggled: (deploy: boolean) => void - onConfigBundlesSelected: (configBundleId?: string[]) => void } -const mergeInstancePatch = (instance: Instance, message: InstanceUpdatedMessage): Instance => ({ - ...instance, - config: { - ...instance.config, - ...message, - }, -}) - const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, DeploymentActions] => { const { t } = useTranslation('deployments') const routes = useTeamRoutes() @@ -107,13 +78,9 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, const { deployment: optionDeploy, onWsError, onApiError } = options const { project, version } = optionDeploy - const throttle = useThrottling(DEPLOYMENT_EDIT_WS_REQUEST_DELAY) - - const patch = useRef>({}) - const [deployment, setDeployment] = useState(optionDeploy) const [node, setNode] = useNodeState(optionDeploy.node) - const [saveState, setSaveState] = useState(null) + const [saveState, setSaveState] = useState('disconnected') const [editState, setEditState] = useState('details') const [instances, setInstances] = useState(deployment.instances ?? []) const [viewMode, setViewMode] = usePersistedViewMode({ initialViewMode: 'list', pageName: 'deployments' }) @@ -142,49 +109,14 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, const sock = useWebSocket(routes.deployment.detailsSocket(deployment.id), { onOpen: () => setSaveState('connected'), onClose: () => setSaveState('disconnected'), - onSend: message => { - if ([WS_TYPE_PATCH_INSTANCE, WS_TYPE_PATCH_DEPLOYMENT_ENV].includes(message.type)) { - setSaveState('saving') - } - }, - onReceive: message => { - if (WS_TYPE_PATCH_RECEIVED === message.type) { - setSaveState('saved') - } - }, onError: onWsError, }) const editor = useEditorState(sock) - sock.on(WS_TYPE_DEPLOYMENT_ENV_UPDATED, (message: DeploymentEnvUpdatedMessage) => { - setDeployment({ - ...deployment, - ...message, - }) - }) - - sock.on(WS_TYPE_INSTANCE_UPDATED, (message: InstanceUpdatedMessage) => { - const index = instances.findIndex(it => it.id === message.instanceId) - if (index < 0) { - sock.send(WS_TYPE_GET_INSTANCE, { - id: message.instanceId, - } as GetInstanceMessage) - return - } - - const oldOne = instances[index] - const instance = mergeInstancePatch(oldOne, message) - - const newInstances = [...instances] - newInstances[index] = instance - - setInstances(newInstances) - }) - - sock.on(WS_TYPE_INSTANCE, (message: InstanceMessage) => setInstances([...instances, message])) - - sock.on(WS_TYPE_INSTANCES_ADDED, (message: InstancesAddedMessage) => setInstances([...instances, ...message])) + sock.on(WS_TYPE_INSTANCES_ADDED, (message: InstancesAddedMessage) => + setInstances([...instances, ...message.map(it => instanceCreatedMessageToInstance(it))]), + ) sock.on(WS_TYPE_IMAGE_DELETED, (message: ImageDeletedMessage) => setInstances(instances.filter(it => it.image.id !== message.imageId)), @@ -195,124 +127,6 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, setEditState('details') } - const onEnvironmentEdited = environment => { - setSaveState('saving') - setDeployment({ - ...deployment, - environment, - }) - throttle(() => { - sock.send(WS_TYPE_PATCH_DEPLOYMENT_ENV, { - environment, - }) - }) - } - - const onConfigBundlesSelected = configBundleIds => { - setSaveState('saving') - setDeployment({ - ...deployment, - configBundleIds, - }) - throttle(() => { - sock.send(WS_TYPE_PATCH_DEPLOYMENT_ENV, { - configBundleIds, - }) - }) - } - - const onInvalidateSecrets = (secrets: DeploymentInvalidatedSecrets[]) => { - const newInstances = instances.map(it => { - const invalidated = secrets.find(sec => sec.instanceId === it.id) - if (!invalidated) { - return it - } - - return { - ...it, - config: { - ...(it.config ?? {}), - secrets: (it.config?.secrets ?? []).map(secret => { - if (invalidated.invalid.includes(secret.id)) { - return { - ...secret, - encrypted: false, - publicKey: '', - value: '', - } - } - - return secret - }), - }, - } - }) - - setInstances(newInstances) - } - - const onPatchInstance = (id: string, newConfig: InstanceContainerConfigData) => { - const index = instances.findIndex(it => it.id === id) - if (index < 0) { - return - } - - setSaveState('saving') - - const newPatch = { - ...patch.current, - ...newConfig, - } - patch.current = newPatch - - const newInstances = [...instances] - const instance = newInstances[index] - - newInstances[index] = { - ...instance, - config: instance.config - ? { - ...instance.config, - ...newConfig, - } - : newConfig, - } - - setInstances(newInstances) - - throttle(() => { - sock.send(WS_TYPE_PATCH_INSTANCE, { - instanceId: id, - config: patch.current, - } as PatchInstanceMessage) - patch.current = {} - }) - } - - const updateInstanceConfig = (id: string, newConfig: InstanceContainerConfigData) => { - const index = instances.findIndex(it => it.id === id) - if (index < 0) { - return - } - - setSaveState('saving') - - const newInstances = [...instances] - const instance = newInstances[index] - - newInstances[index] = { - ...instance, - config: instance.config - ? { - ...instance.config, - ...newConfig, - } - : newConfig, - } - - setInstances(newInstances) - } - const onDeploymentTokenCreated = (token: DeploymentToken) => { setDeployment({ ...deployment, @@ -382,16 +196,11 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, { setEditState, onDeploymentEdited, - onEnvironmentEdited, setViewMode, - onInvalidateSecrets, - onPatchInstance, onDeploymentTokenCreated, onRevokeDeploymentToken, onInstanceSelected, onAllInstancesToggled, - updateInstanceConfig, - onConfigBundlesSelected, }, ] } diff --git a/web/crux-ui/src/components/nodes/node-containers-list.tsx b/web/crux-ui/src/components/nodes/node-containers-list.tsx index b0b12f6fef..606dab5dcf 100644 --- a/web/crux-ui/src/components/nodes/node-containers-list.tsx +++ b/web/crux-ui/src/components/nodes/node-containers-list.tsx @@ -18,12 +18,12 @@ import { containerIsStopable, containerPortsToString, containerPrefixNameOf, - imageName, + imageNameOfContainer, } from '@app/models' import { utcDateToLocale } from '@app/utils' import useTranslation from 'next-translate/useTranslation' -interface NodeContainersListProps { +type NodeContainersListProps = { state: NodeDetailsState actions: NodeDetailsActions showHidden?: boolean @@ -57,12 +57,13 @@ const NodeContainersList = (props: NodeContainersListProps) => { header={t('images:imageTag')} className="w-3/12" sortable - sortField={(it: Container) => imageName(it.imageName, it.imageTag)} + sortField={imageNameOfContainer} sort={sortString} bodyClassName="truncate" - body={(it: Container) => ( - {imageName(it.imageName, it.imageTag)} - )} + body={(it: Container) => { + const name = imageNameOfContainer(it) + return {name} + }} /> +type FilterState = { + filter: string + status: DeploymentStatus | null +} const NodeDeploymentList = (props: NodeDeploymentListProps) => { - const { deployments: propsDeployments } = props + const { nodeId: propsNodeId } = props const { t } = useTranslation('deployments') const routes = useTeamRoutes() const router = useRouter() + const handleApiError = defaultApiErrorHandler(t) + const [showInfo, setShowInfo] = useState(null) - const filters = useFilters({ - filters: [ - textFilterFor(it => [it.project.name, it.version.name, it.prefix]), - enumFilterFor(it => [it.status]), - ], - initialData: propsDeployments, + const [filter, setFilter] = useState({ + filter: '', + status: null, }) + const throttle = useThrottling(1000) + const onRowClick = async (data: Deployment) => await router.push(routes.deployment.details(data.id)) + const fetchData = useCallback( + async (paginationQuery: PaginationQuery): Promise> => { + const { filter: keywordFilter, status } = filter + + const query: DeploymentQuery = { + ...paginationQuery, + filter: !keywordFilter || keywordFilter.trim() === '' ? null : keywordFilter, + status, + } + + const res = await fetch(routes.node.api.deployments(propsNodeId, query)) + + if (!res.ok) { + await handleApiError(res) + return null + } + + return (await res.json()) as PaginatedList + }, + [routes, handleApiError, filter], + ) + + const [pagination, setPagination, refreshPage] = usePagination( + { + defaultSettings: defaultPagination, + fetchData, + }, + [filter], + ) + + useEffect(() => { + throttle(refreshPage) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filter]) + return ( <> - {propsDeployments.length ? ( - <> - filters.setFilter({ text: it })}> - t(deploymentStatusTranslation(it))} - selection={filters.filter?.enum} - onSelectionChange={type => { - filters.setFilter({ - enum: type, - }) - }} - qaLabel={chipsQALabelFromValue} - /> - - - - - it.project.name} - className="w-3/12" - sortable - sortField="project.name" - sort={sortString} - /> - it.version.name} - className="w-1/12" - sortable - sortField="version.name" - sort={sortString} - /> - - auditToLocaleDate(it.audit)} - className="w-2/12" - suppressHydrationWarning - sortable - sortField={it => it.audit.updatedAt ?? it.audit.createdAt} - sort={sortDate} - /> - } - className="text-center" - sortable - sortField="status" - sort={sortEnum(DEPLOYMENT_STATUS_VALUES)} - /> - ( - <> -
- - - -
- - 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30'} - onClick={() => !!it.note && it.note.length > 0 && setShowInfo(it)} - /> - - )} - /> -
-
- - ) : ( - - {t('noItems')} - - )} + setFilter({ ...filter, filter: it })}> + t(deploymentStatusTranslation(it))} + selection={filter.status} + onSelectionChange={type => { + setFilter({ + ...filter, + status: type === 'all' ? null : (type as DeploymentStatus), + }) + }} + qaLabel={chipsQALabelFromValue} + /> + + + + + it.project.name} + className="w-3/12" + sortable + sortField="project.name" + sort={sortString} + /> + it.version.name} + className="w-1/12" + sortable + sortField="version.name" + sort={sortString} + /> + + auditToLocaleDate(it.audit)} + className="w-2/12" + suppressHydrationWarning + sortable + sortField={it => it.audit.updatedAt ?? it.audit.createdAt} + sort={sortDate} + /> + } + className="text-center" + sortable + sortField="status" + sort={sortEnum(DEPLOYMENT_STATUS_VALUES)} + /> + ( + <> +
+ + + +
+ + 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30'} + onClick={() => !!it.note && it.note.length > 0 && setShowInfo(it)} + /> + + )} + /> +
+
{showInfo && ( Promise errorMessage?: string | null + allowNull?: boolean + onSelectionChange: (node: DyoNode | null) => Promise onNodesFetched?: (nodes: DyoNode[] | null) => void } const SelectNodeChips = (props: SelectNodeChipsProps) => { - const { className, name, selection, onSelectionChange, errorMessage, onNodesFetched } = props + const { className, name, selection, onSelectionChange, errorMessage, onNodesFetched, allowNull } = props const { t } = useTranslation('common') const routes = useTeamRoutes() @@ -31,6 +32,9 @@ const SelectNodeChips = (props: SelectNodeChipsProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodes]) + const baseChoices = allowNull ? [null] : [] + const choices = [...baseChoices, ...(nodes ?? [])] + return fetchError ? ( {t('errors:fetchFailed', { @@ -46,8 +50,8 @@ const SelectNodeChips = (props: SelectNodeChipsProps) => { it.name} + choices={choices} + converter={(it: DyoNode | null) => (allowNull === true && it == null ? t('none') : it.name)} selection={selection} onSelectionChange={onSelectionChange} /> diff --git a/web/crux-ui/src/components/projects/versions/images/add-images-card.tsx b/web/crux-ui/src/components/projects/versions/images/add-images-card.tsx index 6491191663..569dcb8c45 100644 --- a/web/crux-ui/src/components/projects/versions/images/add-images-card.tsx +++ b/web/crux-ui/src/components/projects/versions/images/add-images-card.tsx @@ -1,4 +1,4 @@ -import { IMAGE_FILTER_MIN_LENGTH, IMAGE_WS_REQUEST_DELAY } from '@app/const' +import { IMAGE_FILTER_MIN_LENGTH, WS_PATCH_DELAY } from '@app/const' import DyoButton from '@app/elements/dyo-button' import { DyoCard } from '@app/elements/dyo-card' import DyoCheckbox from '@app/elements/dyo-checkbox' @@ -47,7 +47,7 @@ const AddImagesCard = (props: AddImagesCardProps) => { const [images, setImages] = useState([]) const [filterOrName, setFilterOrName] = useState('') const [inputMessage, setInputMessage] = useState(null) - const throttleFilter = useThrottling(IMAGE_WS_REQUEST_DELAY) + const throttleFilter = useThrottling(WS_PATCH_DELAY) const registriesFound = registries?.length > 0 diff --git a/web/crux-ui/src/components/projects/versions/images/config/config-section-label.tsx b/web/crux-ui/src/components/projects/versions/images/config/config-section-label.tsx deleted file mode 100644 index d64a1dccb7..0000000000 --- a/web/crux-ui/src/components/projects/versions/images/config/config-section-label.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import DyoIcon from '@app/elements/dyo-icon' -import { DyoLabel } from '@app/elements/dyo-label' -import clsx from 'clsx' - -type ConfigSectionLabelProps = { - disabled: boolean - className?: string - labelClassName?: string - children: React.ReactNode - onResetSection: VoidFunction -} - -const ConfigSectionLabel = (props: ConfigSectionLabelProps) => { - const { disabled, className, labelClassName, children, onResetSection } = props - - return ( -
- {children} - - {disabled ? null : ( - - )} -
- ) -} - -export default ConfigSectionLabel diff --git a/web/crux-ui/src/components/projects/versions/images/edit-image-card.tsx b/web/crux-ui/src/components/projects/versions/images/edit-image-card.tsx deleted file mode 100644 index ff3bd4c4f2..0000000000 --- a/web/crux-ui/src/components/projects/versions/images/edit-image-card.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import useItemEditorState from '@app/components/editor/use-item-editor-state' -import { DyoCard } from '@app/elements/dyo-card' -import DyoImgButton from '@app/elements/dyo-img-button' -import DyoMessage from '@app/elements/dyo-message' -import { DyoConfirmationModal } from '@app/elements/dyo-modal' -import { - imageConfigToJsonContainerConfig, - JsonContainerConfig, - mergeJsonConfigToImageContainerConfig, - VersionImage, -} from '@app/models' -import { createContainerConfigSchema, getValidationError } from '@app/validations' -import useTranslation from 'next-translate/useTranslation' -import { VerionState, VersionActions } from '../use-version-state' -import EditImageHeading from './edit-image-heading' -import EditImageJson from './edit-image-json' -import useImageEditorState from './use-image-editor-state' - -interface EditImageCardProps { - disabled?: boolean - image: VersionImage - imagesState: VerionState - imagesActions: VersionActions -} - -const EditImageCard = (props: EditImageCardProps) => { - const { disabled, image, imagesState, imagesActions } = props - const { editor, versionSock } = imagesState - - const { t } = useTranslation('images') - - const [state, actions] = useImageEditorState({ - image, - imagesState, - imagesActions, - sock: versionSock, - t, - }) - - const editorState = useItemEditorState(editor, versionSock, image.id) - const errorMessage = - state.parseError ?? getValidationError(createContainerConfigSchema(image.labels), image.config, null, t)?.message - - return ( - <> - -
- - - {disabled ? null : ( - - )} -
- - {errorMessage ? ( - - ) : null} - -
- - actions.onPatch(mergeJsonConfigToImageContainerConfig(image.config, it)) - } - onParseError={actions.setParseError} - convertConfigToJson={imageConfigToJsonContainerConfig} - /> -
-
- - - - ) -} - -export default EditImageCard diff --git a/web/crux-ui/src/components/projects/versions/images/use-image-editor-state.ts b/web/crux-ui/src/components/projects/versions/images/use-image-editor-state.ts deleted file mode 100644 index dc5184d4d4..0000000000 --- a/web/crux-ui/src/components/projects/versions/images/use-image-editor-state.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { IMAGE_WS_REQUEST_DELAY } from '@app/const' -import { DyoConfirmationModalConfig } from '@app/elements/dyo-modal' -import useConfirmation from '@app/hooks/use-confirmation' -import { useThrottling } from '@app/hooks/use-throttleing' -import { - ContainerConfigData, - DeleteImageMessage, - PatchImageMessage, - VersionImage, - WS_TYPE_DELETE_IMAGE, - WS_TYPE_PATCH_IMAGE, -} from '@app/models' -import WebSocketClientEndpoint from '@app/websockets/websocket-client-endpoint' -import { Translate } from 'next-translate' -import { useRef, useState } from 'react' -import { selectTagsOfImage, VerionState, VersionActions } from '../use-version-state' -import { QA_DIALOG_LABEL_DELETE_IMAGE } from 'quality-assurance' - -export type ImageEditorState = { - image: VersionImage - tags: string[] - parseError: string - deleteModal: DyoConfirmationModalConfig - imageEditorSock: WebSocketClientEndpoint -} - -export type ImageEditorActions = { - selectTag: (tag: string) => void - onPatch: (config: Partial) => void - deleteImage: VoidFunction - setParseError: (error: Error) => void -} - -export type ImageEditorStateOptions = { - image: VersionImage - imagesState: VerionState - imagesActions: VersionActions - sock: WebSocketClientEndpoint - t: Translate -} - -const useImageEditorState = (options: ImageEditorStateOptions): [ImageEditorState, ImageEditorActions] => { - const { image, imagesState, imagesActions, sock, t } = options - - const [deleteModal, confirmDelete] = useConfirmation() - const [parseError, setParseError] = useState(null) - - const patch = useRef>({}) - const throttle = useThrottling(IMAGE_WS_REQUEST_DELAY) - - const tags = selectTagsOfImage(imagesState, image) - - const selectTag = (tag: string) => imagesActions.selectTagForImage(image, tag) - - const onPatch = (config: Partial) => { - setParseError(null) - imagesActions.updateImageConfig(image, config) - - const newPatch = { - ...patch.current, - ...config, - } - patch.current = newPatch - - throttle(() => { - sock.send(WS_TYPE_PATCH_IMAGE, { - id: image.id, - config: patch.current, - } as PatchImageMessage) - - patch.current = {} - }) - } - - const deleteImage = async () => { - const confirmed = await confirmDelete({ - qaLabel: QA_DIALOG_LABEL_DELETE_IMAGE, - title: t('common:areYouSureDeleteName', { name: image.name }), - description: t('common:proceedYouLoseAllDataToName', { name: image.name }), - confirmText: t('common:delete'), - confirmColor: 'bg-error-red', - }) - - if (!confirmed) { - return - } - - sock.send(WS_TYPE_DELETE_IMAGE, { - imageId: image.id, - } as DeleteImageMessage) - } - - return [ - { - image, - tags, - deleteModal, - parseError, - imageEditorSock: sock, - }, - { - selectTag, - onPatch, - deleteImage, - setParseError: (err: Error) => setParseError(err.message), - }, - ] -} - -export default useImageEditorState diff --git a/web/crux-ui/src/components/projects/versions/use-version-state.ts b/web/crux-ui/src/components/projects/versions/use-version-state.ts index 6c6c496b0b..b93c0bc994 100644 --- a/web/crux-ui/src/components/projects/versions/use-version-state.ts +++ b/web/crux-ui/src/components/projects/versions/use-version-state.ts @@ -14,9 +14,8 @@ import { ImageMessage, ImagesAddedMessage, ImagesWereReorderedMessage, - ImageUpdateMessage, + ImageTagMessage, OrderImagesMessage, - PatchImageMessage, PatchVersionImage, RegistryImages, RegistryImageTags, @@ -28,12 +27,13 @@ import { WS_TYPE_ADD_IMAGES, WS_TYPE_GET_IMAGE, WS_TYPE_IMAGE, + WS_TYPE_IMAGE_DELETED, + WS_TYPE_SET_IMAGE_TAG, + WS_TYPE_IMAGE_TAG_UPDATED, WS_TYPE_IMAGES_ADDED, WS_TYPE_IMAGES_WERE_REORDERED, - WS_TYPE_IMAGE_DELETED, - WS_TYPE_IMAGE_UPDATED, WS_TYPE_ORDER_IMAGES, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_RECEIVED, WS_TYPE_REGISTRY_FETCH_IMAGE_TAGS, WS_TYPE_REGISTRY_IMAGE_TAGS, @@ -159,7 +159,7 @@ export const useVersionState = (options: VersionStateOptions): [VerionState, Ver onOpen: viewMode !== 'tile' ? null : () => setSaveState('connected'), onClose: viewMode !== 'tile' ? null : () => setSaveState('disconnected'), onSend: message => { - if (message.type === WS_TYPE_PATCH_IMAGE) { + if (message.type === WS_TYPE_SET_IMAGE_TAG || message.type === WS_TYPE_PATCH_CONFIG) { setSaveState('saving') } }, @@ -217,17 +217,20 @@ export const useVersionState = (options: VersionStateOptions): [VerionState, Ver setVersion({ ...version, images: newImages }) }) - versionSock.on(WS_TYPE_IMAGE_UPDATED, (message: ImageUpdateMessage) => { - const index = version.images.findIndex(it => it.id === message.id) + versionSock.on(WS_TYPE_IMAGE_TAG_UPDATED, (message: ImageTagMessage) => { + const index = version.images.findIndex(it => it.id === message.imageId) if (index < 0) { versionSock.send(WS_TYPE_GET_IMAGE, { - id: message.id, + id: message.imageId, } as GetImageMessage) return } const oldImage = version.images[index] - const image = mergeImagePatch(oldImage, message) + + const image = mergeImagePatch(oldImage, { + tag: message.tag, + }) const newImages = [...version.images] newImages[index] = image @@ -314,10 +317,10 @@ export const useVersionState = (options: VersionStateOptions): [VerionState, Ver setVersion({ ...version, images: newImages }) - versionSock.send(WS_TYPE_PATCH_IMAGE, { - id: image.id, + versionSock.send(WS_TYPE_SET_IMAGE_TAG, { + imageId: image.id, tag, - } as PatchImageMessage) + } as ImageTagMessage) } const updateImageConfig = (image: VersionImage, config: Partial) => { diff --git a/web/crux-ui/src/components/projects/versions/version-images-section.tsx b/web/crux-ui/src/components/projects/versions/version-images-section.tsx index 532eef107f..f3e5c12184 100644 --- a/web/crux-ui/src/components/projects/versions/version-images-section.tsx +++ b/web/crux-ui/src/components/projects/versions/version-images-section.tsx @@ -19,7 +19,7 @@ const VersionImagesSection = (props: VersionImagesSectionProps) => { return version.images.length ? ( viewMode === 'tile' ? ( - + ) : ( ) diff --git a/web/crux-ui/src/components/projects/versions/version-view-list.tsx b/web/crux-ui/src/components/projects/versions/version-view-list.tsx index da31031165..6a8b19359b 100644 --- a/web/crux-ui/src/components/projects/versions/version-view-list.tsx +++ b/web/crux-ui/src/components/projects/versions/version-view-list.tsx @@ -5,7 +5,7 @@ import DyoModal, { DyoConfirmationModal } from '@app/elements/dyo-modal' import DyoTable, { DyoColumn, sortDate, sortNumber, sortString } from '@app/elements/dyo-table' import useConfirmation from '@app/hooks/use-confirmation' import useTeamRoutes from '@app/hooks/use-team-routes' -import { DeleteImageMessage, VersionImage, WS_TYPE_DELETE_IMAGE } from '@app/models' +import { DeleteImageMessage, containerNameOfImage, VersionImage, WS_TYPE_DELETE_IMAGE } from '@app/models' import { utcDateToLocale } from '@app/utils' import useTranslation from 'next-translate/useTranslation' import { QA_DIALOG_LABEL_DELETE_IMAGE, QA_MODAL_LABEL_IMAGE_TAGS } from 'quality-assurance' @@ -62,7 +62,13 @@ const VersionViewList = (props: VersionViewListProps) => { sort={sortNumber} body={data => `#${data.order + 1}`} /> - + { onClick={() => onOpenTagsDialog(it)} />
+
{ onClick={() => onDelete(it)} />
- - + + + )} diff --git a/web/crux-ui/src/components/projects/versions/version-view-tile.tsx b/web/crux-ui/src/components/projects/versions/version-view-tile.tsx index 4212fd6ff7..ea855aab36 100644 --- a/web/crux-ui/src/components/projects/versions/version-view-tile.tsx +++ b/web/crux-ui/src/components/projects/versions/version-view-tile.tsx @@ -1,26 +1,54 @@ import DyoWrap from '@app/elements/dyo-wrap' +import { + ContainerConfigParent, + containerConfigToJsonConfig, + DeleteImageMessage, + mergeJsonWithContainerConfig, + VersionImage, + WS_TYPE_DELETE_IMAGE, +} from '@app/models' import clsx from 'clsx' -import EditImageCard from './images/edit-image-card' -import { VerionState, VersionActions } from './use-version-state' +import EditContainerConfigCard from '../../container-configs/edit-container-config-card' +import { VerionState } from './use-version-state' interface VersionViewTileProps { disabled?: boolean state: VerionState - actions: VersionActions } const VersionViewTile = (props: VersionViewTileProps) => { - const { disabled, state, actions } = props + const { disabled, state } = props + + const onDelete = (it: VersionImage) => { + state.versionSock.send(WS_TYPE_DELETE_IMAGE, { + imageId: it.id, + } as DeleteImageMessage) + } return ( {state.version.images .sort((one, other) => one.order - other.order) - .map((it, index) => ( -
- -
- ))} + .map((it, index) => { + const parent: ContainerConfigParent = { + id: it.id, + name: it.name, + mutable: !disabled, + } + + return ( +
+ onDelete(it)} + convertConfigToJson={containerConfigToJsonConfig} + mergeJsonWithConfig={mergeJsonWithContainerConfig} + /> +
+ ) + })}
) } diff --git a/web/crux-ui/src/components/shared/key-only-input.tsx b/web/crux-ui/src/components/shared/key-only-input.tsx index 72158142c0..2f61a68f1f 100644 --- a/web/crux-ui/src/components/shared/key-only-input.tsx +++ b/web/crux-ui/src/components/shared/key-only-input.tsx @@ -6,7 +6,7 @@ import { useEffect } from 'react' import { v4 as uuid } from 'uuid' import MultiInput from '../editor/multi-input' import { ItemEditorState } from '../editor/use-item-editor-state' -import ConfigSectionLabel from '../projects/versions/images/config/config-section-label' +import ConfigSectionLabel from '../container-configs/config-section-label' const EMPTY_KEY = { id: uuid(), @@ -121,8 +121,6 @@ const KeyOnlyInput = (props: KeyInputProps) => { } const onResetSection = () => { - dispatch(mergeItems(items)) - propsOnResetSection() } diff --git a/web/crux-ui/src/components/shared/key-value-input.tsx b/web/crux-ui/src/components/shared/key-value-input.tsx index 1ef6336132..448bda708f 100644 --- a/web/crux-ui/src/components/shared/key-value-input.tsx +++ b/web/crux-ui/src/components/shared/key-value-input.tsx @@ -1,16 +1,16 @@ import { MessageType } from '@app/elements/dyo-input' import useRepatch from '@app/hooks/use-repatch' +import DyoMessage from '@app/elements/dyo-message' import { UniqueKeyValue } from '@app/models' +import { ErrorWithPath } from '@app/validations' import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' import { Fragment, HTMLInputTypeAttribute, useEffect } from 'react' import { v4 as uuid } from 'uuid' +import ConfigSectionLabel from '../container-configs/config-section-label' import MultiInput from '../editor/multi-input' import { ItemEditorState } from '../editor/use-item-editor-state' -import ConfigSectionLabel from '../projects/versions/images/config/config-section-label' -import { ErrorWithPath } from '@app/validations' -import DyoMessage from '@app/elements/dyo-message' const EMPTY_KEY_VALUE_PAIR = { id: uuid(), @@ -65,7 +65,7 @@ const mergeItems = return result } -interface KeyValueInputProps { +type KeyValueInputProps = { disabled?: boolean valueDisabled?: boolean className?: string @@ -80,7 +80,7 @@ interface KeyValueInputProps { messageType?: MessageType onChange: (items: UniqueKeyValue[]) => void onResetSection?: VoidFunction - hint?: (key: string) => string | undefined + errors?: Record findErrorMessage?: (index: number) => ErrorWithPath } @@ -100,7 +100,7 @@ const KeyValueInput = (props: KeyValueInputProps) => { messageType, onChange: propsOnChange, onResetSection: propsOnResetSection, - hint, + errors = {}, findErrorMessage, } = props @@ -114,11 +114,11 @@ const KeyValueInput = (props: KeyValueInputProps) => { keyValues.forEach((item, index) => { const error = findErrorMessage?.call(null, index) const keyUniqueErr = result.find(it => it.key === item.key) ? t('keyMustUnique') : null - const hintErr = !keyUniqueErr && hint ? hint(item.key) : null + const itemError = (!keyUniqueErr && errors[item.key]) ?? null result.push({ ...item, - message: keyUniqueErr ?? hintErr ?? error?.message, - messageType: (keyUniqueErr || error ? 'error' : 'info') as MessageType, + message: keyUniqueErr ?? itemError ?? error?.message, + messageType: 'error' as MessageType, }) }) @@ -145,8 +145,6 @@ const KeyValueInput = (props: KeyValueInputProps) => { } const onResetSection = () => { - dispatch(mergeItems([])) - propsOnResetSection() } diff --git a/web/crux-ui/src/components/shared/secret-key-input.tsx b/web/crux-ui/src/components/shared/secret-key-input.tsx index 6bad5c8443..314caf7eb5 100644 --- a/web/crux-ui/src/components/shared/secret-key-input.tsx +++ b/web/crux-ui/src/components/shared/secret-key-input.tsx @@ -7,7 +7,7 @@ import { useEffect } from 'react' import { v4 as uuid } from 'uuid' import MultiInput from '../editor/multi-input' import { ItemEditorState } from '../editor/use-item-editor-state' -import ConfigSectionLabel from '../projects/versions/images/config/config-section-label' +import ConfigSectionLabel from '../container-configs/config-section-label' const EMPTY_KEY = { id: uuid(), diff --git a/web/crux-ui/src/const.ts b/web/crux-ui/src/const.ts index d3bb2ef5b7..d8fb4a067b 100644 --- a/web/crux-ui/src/const.ts +++ b/web/crux-ui/src/const.ts @@ -3,12 +3,12 @@ export const HOUR_IN_SECONDS = 3600 export const NODE_SETUP_SCRIPT_TIMEOUT = 600 // 10 min in seconds export const GRPC_STREAM_RECONNECT_TIMEOUT = 5_000 // millis export const IMAGE_FILTER_MIN_LENGTH = 1 // characters -export const IMAGE_WS_REQUEST_DELAY = 500 // millis export const INSTANCE_WS_REQUEST_DELAY = 500 // millis export const DEPLOYMENT_EDIT_WS_REQUEST_DELAY = 500 // millis export const CONFIG_BUNDLE_EDIT_WS_REQUEST_DELAY = 500 // millis export const WS_CONNECT_DELAY_PER_TRY = 5_000 // millis export const WS_MAX_CONNECT_TRY = 20 +export const WS_PATCH_DELAY = 500 // millis export const REGISTRY_HUB_URL = 'hub.docker.com' export const REGISTRY_GITHUB_URL = 'ghcr.io' @@ -34,6 +34,7 @@ export const KRATOS_ERROR_NO_VERIFIED_EMAIL_ADDRESS = 4000010 export const STORAGE_VIEW_MODE = 'view-mode' +export const UID_MIN = -1 export const UID_MAX = 2147483647 export const STORAGE_TEAM_SLUG = 'teamSlug' diff --git a/web/crux-ui/src/elements/dyo-modal.tsx b/web/crux-ui/src/elements/dyo-modal.tsx index 2c209a435e..e88618acf6 100644 --- a/web/crux-ui/src/elements/dyo-modal.tsx +++ b/web/crux-ui/src/elements/dyo-modal.tsx @@ -122,10 +122,10 @@ export const DyoConfirmationModal = (props: DyoConfirmationModalProps) => { onClose(false)} - qaLabel={config?.qaLabel} + qaLabel={config.qaLabel} buttons={ <> onClose(true)}> diff --git a/web/crux-ui/src/elements/dyo-multi-select.tsx b/web/crux-ui/src/elements/dyo-multi-select.tsx index 9c2470345e..ea74d62de0 100644 --- a/web/crux-ui/src/elements/dyo-multi-select.tsx +++ b/web/crux-ui/src/elements/dyo-multi-select.tsx @@ -1,11 +1,16 @@ import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import DyoCheckbox from './dyo-checkbox' import DyoIcon from './dyo-icon' import DyoMessage from './dyo-message' -export type DyoMultiSelectProps = { +type MutliSelectItem = { + id: string + name: string +} + +export type DyoMultiSelectProps = { className?: string name: string disabled?: boolean @@ -13,13 +18,11 @@ export type DyoMultiSelectProps = { message?: string messageType?: 'error' | 'info' choices: readonly T[] - selection: string[] - idConverter: (choice: T) => string - labelConverter: (choice: T) => string - onSelectionChange: (selection: string[]) => void + selection: T[] + onSelectionChange: (selection: T[]) => void } -const DyoMultiSelect = (props: DyoMultiSelectProps) => { +const DyoMultiSelect = (props: DyoMultiSelectProps) => { const { message, messageType, @@ -27,9 +30,7 @@ const DyoMultiSelect = (props: DyoMultiSelectProps) => { className, grow, choices, - selection, - idConverter, - labelConverter, + selection: propsSelection, onSelectionChange, name, } = props @@ -38,7 +39,12 @@ const DyoMultiSelect = (props: DyoMultiSelectProps) => { const [selectorVisible, setSelectorVisible] = useState(false) - const toggleSelection = (ev: React.MouseEvent | React.ChangeEvent, id: string) => { + const selection = useMemo( + () => choices.filter(choice => propsSelection.find(it => it.id === choice.id)), + [choices, propsSelection], + ) + + const toggleSelection = (ev: React.MouseEvent | React.ChangeEvent, item: T) => { if (disabled) { return } @@ -46,15 +52,10 @@ const DyoMultiSelect = (props: DyoMultiSelectProps) => { ev.preventDefault() ev.stopPropagation() - if (selection.includes(id)) { - const newSelection = [...selection] - const index = selection.indexOf(id) - if (index >= 0) { - newSelection.splice(index, 1) - } - onSelectionChange(newSelection) + if (selection.includes(item)) { + onSelectionChange(selection.filter(it => it !== item)) } else { - onSelectionChange([...selection, id]) + onSelectionChange([...selection, item]) } } @@ -98,17 +99,7 @@ const DyoMultiSelect = (props: DyoMultiSelectProps) => { )} onClick={toggleDropdown} > - +
@@ -117,24 +108,21 @@ const DyoMultiSelect = (props: DyoMultiSelectProps) => { {selectorVisible && (
- {choices.map((it, index) => { - const itemId = idConverter(it) - return ( -
toggleSelection(e, itemId)} - > - toggleSelection(ev, itemId)} - qaLabel={`${name}-${index}`} - /> - -
- ) - })} + {choices.map((it, index) => ( +
toggleSelection(e, it)} + > + toggleSelection(ev, it)} + qaLabel={`${name}-${index}`} + /> + +
+ ))}
)}
diff --git a/web/crux-ui/src/hooks/use-deploy.ts b/web/crux-ui/src/hooks/use-deploy.ts index 6c8b2c4f56..b151730565 100644 --- a/web/crux-ui/src/hooks/use-deploy.ts +++ b/web/crux-ui/src/hooks/use-deploy.ts @@ -1,19 +1,19 @@ import { defaultApiErrorHandler } from '@app/errors' -import { DeploymentDetails, DyoApiError, StartDeployment, mergeConfigs } from '@app/models' +import { DeploymentDetails, DyoApiError, mergeConfigsWithConcreteConfig, StartDeployment } from '@app/models' import { TeamRoutes } from '@app/routes' import { sendForm } from '@app/utils' -import { Translate } from 'next-translate' -import { NextRouter } from 'next/router' -import { DyoConfirmationAction } from './use-confirmation' import { getValidationError, startDeploymentSchema, validationErrorToInstance, yupErrorTranslate, } from '@app/validations' +import { Translate } from 'next-translate' import useTranslation from 'next-translate/useTranslation' -import toast from 'react-hot-toast' +import { NextRouter } from 'next/router' import { QA_DIALOG_LABEL_DEPLOY_PROTECTED } from 'quality-assurance' +import toast from 'react-hot-toast' +import { DyoConfirmationAction } from './use-confirmation' export type UseDeployOptions = { router: NextRouter @@ -57,7 +57,10 @@ export const useDeploy = (opts: UseDeployOptions): UseDeployAction => { ...deployment, instances: selectedInstances.map(it => ({ ...it, - config: mergeConfigs(it.image.config, it.config), + config: { + ...it.config, + ...mergeConfigsWithConcreteConfig([it.image.config], it.config), + }, })), } @@ -73,7 +76,7 @@ export const useDeploy = (opts: UseDeployOptions): UseDeployAction => { ...translatedError, path: intanceIndex !== null - ? selectedInstances[intanceIndex].config?.name ?? selectedInstances[intanceIndex].image.config.name + ? selectedInstances[intanceIndex].config.name ?? selectedInstances[intanceIndex].image.config.name : translatedError.path, }), { @@ -158,7 +161,7 @@ export const useDeploy = (opts: UseDeployOptions): UseDeployAction => { t('errors:validationFailedForInstance', { path: intanceIndex !== null - ? deployment.instances[intanceIndex].config?.name ?? + ? deployment.instances[intanceIndex].config.name ?? deployment.instances[intanceIndex].image.config.name : property, }), diff --git a/web/crux-ui/src/hooks/use-pagination.ts b/web/crux-ui/src/hooks/use-pagination.ts index 4909d0deb3..6e568bec53 100644 --- a/web/crux-ui/src/hooks/use-pagination.ts +++ b/web/crux-ui/src/hooks/use-pagination.ts @@ -1,6 +1,6 @@ import { PaginationSettings } from '@app/components/shared/paginator' import { PaginatedList, PaginationQuery } from '@app/models' -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react' +import { DependencyList, Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react' export type PaginationOptions = { defaultSettings: PaginationSettings @@ -9,13 +9,18 @@ export type PaginationOptions = { export type DispatchPagination = Dispatch> +export type RefreshPageFunc = () => void + export type Pagination = { total: number settings: PaginationSettings data: T[] } -const usePagination = (options: PaginationOptions): [Pagination, DispatchPagination] => { +const usePagination = ( + options: PaginationOptions, + deps?: DependencyList, +): [Pagination, DispatchPagination, RefreshPageFunc] => { const [total, setTotal] = useState(0) const [settings, setSettings] = useState(options.defaultSettings) const [data, setData] = useState(null) @@ -39,7 +44,7 @@ const usePagination = (options: PaginationOptions): [Pagination, Dispat setData(list.items) setTotal(list.total) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [settings]) + }, [settings, ...(deps ?? [])]) useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -54,6 +59,7 @@ const usePagination = (options: PaginationOptions): [Pagination, Dispat total, }, setSettings, + onFetchData, ] } diff --git a/web/crux-ui/src/hooks/use-throttleing.ts b/web/crux-ui/src/hooks/use-throttleing.ts index 53c63473e1..5d518932ec 100644 --- a/web/crux-ui/src/hooks/use-throttleing.ts +++ b/web/crux-ui/src/hooks/use-throttleing.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' type Throttling = (action: VoidFunction, immediate?: boolean) => void @@ -28,9 +28,12 @@ export const useThrottling = (delay: number): Throttling => { return () => clearTimeout(schedule.current) }, [action, delay, schedule]) - return (trigger, resetTimer = false) => - setAction({ - trigger, - resetTimer, - }) + return useCallback( + (trigger, resetTimer = false) => + setAction({ + trigger, + resetTimer, + }), + [], + ) } diff --git a/web/crux-ui/src/hooks/use-websocket-translation.ts b/web/crux-ui/src/hooks/use-websocket-translation.ts index c23a0ede1b..7e311f03a4 100644 --- a/web/crux-ui/src/hooks/use-websocket-translation.ts +++ b/web/crux-ui/src/hooks/use-websocket-translation.ts @@ -14,6 +14,7 @@ const useWebsocketTranslate = (t: Translate) => { if (wsContext.client) { wsContext.client.setErrorHandler(defaultWsErrorHandler(t, router)) } + return () => { if (wsContext.client) { wsContext.client.setErrorHandler(defaultWsErrorHandler(defaultTranslate, router)) diff --git a/web/crux-ui/src/models/compose.ts b/web/crux-ui/src/models/compose.ts index 08d32ab59f..3a32f3e687 100644 --- a/web/crux-ui/src/models/compose.ts +++ b/web/crux-ui/src/models/compose.ts @@ -4,9 +4,9 @@ import { CONTAINER_NETWORK_MODE_VALUES, CONTAINER_VOLUME_TYPE_VALUES, ContainerConfigData, - ContainerConfigPort, - ContainerConfigPortRange, - ContainerConfigVolume, + Port, + ContainerPortRange, + Volume, ContainerRestartPolicyType, UniqueKey, UniqueKeyValue, @@ -96,7 +96,7 @@ const splitPortRange = (range: string): [number, number] | null => { return [Number.parseInt(from, 10), Number.parseInt(to, 10)] } -const mapPort = (port: string | number): [ContainerConfigPort, ContainerConfigPortRange] => { +const mapPort = (port: string | number): [Port, ContainerPortRange] => { try { if (typeof port === 'number') { return [ @@ -154,7 +154,7 @@ const mapPort = (port: string | number): [ContainerConfigPort, ContainerConfigPo } } -const mapVolume = (volume: string): ContainerConfigVolume => { +const mapVolume = (volume: string): Volume => { const [name, path, type] = volume.split(':', 3) return { @@ -184,7 +184,7 @@ const mapUser = (user: string): number => { try { return Number.parseInt(user, 10) } catch { - return -1 + return null } } @@ -225,8 +225,8 @@ export const mapComposeServiceToContainerConfig = ( service: ComposeService, serviceKey: string, ): ContainerConfigData => { - const ports: ContainerConfigPort[] = [] - const portRanges: ContainerConfigPortRange[] = [] + const ports: Port[] = [] + const portRanges: ContainerPortRange[] = [] service.ports?.forEach(it => { const [port, portRange] = mapPort(it) if (port) { diff --git a/web/crux-ui/src/models/config-bundle.ts b/web/crux-ui/src/models/config-bundle.ts index baf1957a9b..26ef53588b 100644 --- a/web/crux-ui/src/models/config-bundle.ts +++ b/web/crux-ui/src/models/config-bundle.ts @@ -1,4 +1,4 @@ -import { UniqueKeyValue } from './container' +import { ContainerConfig, ContainerConfigData } from './container' export type BasicConfigBundle = { id: string @@ -7,10 +7,11 @@ export type BasicConfigBundle = { export type ConfigBundle = BasicConfigBundle & { description?: string + configId: string } -export type ConfigBundleDetails = ConfigBundle & { - environment: UniqueKeyValue[] +export type ConfigBundleDetails = Omit & { + config: ContainerConfig } export type CreateConfigBundle = { @@ -18,25 +19,13 @@ export type CreateConfigBundle = { description?: string } -export type UpdateConfigBundle = CreateConfigBundle & { - environment: UniqueKeyValue[] -} - -export type ConfigBundleOption = BasicConfigBundle - -// ws -export const WS_TYPE_PATCH_CONFIG_BUNDLE = 'patch-config-bundle' -export type PatchConfigBundleMessage = { - name?: string - description?: string - environment?: UniqueKeyValue[] -} - -export const WS_TYPE_CONFIG_BUNDLE_UPDATED = 'config-bundle-updated' -export type ConfigBundleUpdatedMessage = { +export type PatchConfigBundle = { name?: string description?: string - environment?: UniqueKeyValue[] + config?: ContainerConfigData } -export const WS_TYPE_CONFIG_BUNDLE_PATCH_RECEIVED = 'patch-received' +export const detailsToConfigBundle = (bundle: ConfigBundleDetails): ConfigBundle => ({ + ...bundle, + configId: bundle.config.id, +}) diff --git a/web/crux-ui/src/models/container-config.ts b/web/crux-ui/src/models/container-config.ts new file mode 100644 index 0000000000..2fcdb358ca --- /dev/null +++ b/web/crux-ui/src/models/container-config.ts @@ -0,0 +1,45 @@ +import { ConfigBundleDetails } from './config-bundle' +import { ContainerConfig, ContainerConfigData, ContainerConfigKey } from './container' +import { DeploymentWithConfig } from './deployment' +import { VersionImage } from './image' +import { BasicProject } from './project' +import { BasicVersion } from './version' + +export type ContainerConfigRelations = { + project?: BasicProject + version?: BasicVersion + image?: VersionImage + deployment?: DeploymentWithConfig + configBundle?: ConfigBundleDetails +} + +export type ContainerConfigParent = { + id: string + name: string + mutable: boolean +} + +export type ContainerConfigDetails = ContainerConfig & { + parent: ContainerConfigParent + updatedAt?: string + updatedBy?: string +} + +// ws +export const WS_TYPE_PATCH_CONFIG = 'patch-config' +export type PatchConfigMessage = { + config?: ContainerConfigData + resetSection?: ContainerConfigKey +} + +export const WS_TYPE_CONFIG_UPDATED = 'config-updated' +export type ConfigUpdatedMessage = ContainerConfigData & { + id: string +} + +export const WS_TYPE_GET_CONFIG_SECRETS = 'get-config-secrets' +export const WS_TYPE_CONFIG_SECRETS = 'config-secrets' +export type ConfigSecretsMessage = { + keys: string[] + publicKey: string +} diff --git a/web/crux-ui/src/models/container-conflict.ts b/web/crux-ui/src/models/container-conflict.ts new file mode 100644 index 0000000000..039d232d06 --- /dev/null +++ b/web/crux-ui/src/models/container-conflict.ts @@ -0,0 +1,676 @@ +import { + ConcreteContainerConfigData, + ContainerConfigData, + ContainerConfigDataWithId, + ContainerPortRange, + Log, + Marker, + Port, + PortRange, + ResourceConfig, + UniqueKeyValue, + Volume, +} from './container' + +export type ConflictedUniqueItem = { + key: string + configIds: string[] +} + +export type ConflictedPort = { + internal: number + configIds: string[] +} + +export type ConflictedPortRange = { + range: PortRange + configIds: string[] +} + +export type ConflictedLog = { + driver?: string[] + options?: ConflictedUniqueItem[] +} + +export type ConflictedResoureConfig = { + limits?: string[] + requests?: string[] +} + +export type ConflictedMarker = { + service?: ConflictedUniqueItem[] + deployment?: ConflictedUniqueItem[] + ingress?: ConflictedUniqueItem[] +} + +type ConflictedLogKeys = { + driver?: boolean + options?: string[] +} + +type ConflictedResoureConfigKeys = Partial> +type ConflictedMarkerKeys = Partial> + +// config ids where the given property is present +export type ConflictedContainerConfigData = { + // common + name?: string[] + environment?: ConflictedUniqueItem[] + routing?: string[] + expose?: string[] + user?: string[] + workingDirectory?: string[] + tty?: string[] + configContainer?: string[] + ports?: ConflictedPort[] + portRanges?: ConflictedPortRange[] + volumes?: ConflictedUniqueItem[] + initContainers?: string[] + capabilities?: ConflictedUniqueItem[] + storage?: string[] + + // dagent + logConfig?: string[] + restartPolicy?: string[] + networkMode?: string[] + dockerLabels?: ConflictedUniqueItem[] + expectedState?: string[] + + // crane + deploymentStrategy?: string[] + proxyHeaders?: string[] + useLoadBalancer?: string[] + extraLBAnnotations?: ConflictedUniqueItem[] + healthCheckConfig?: string[] + resourceConfig?: string[] + annotations?: ConflictedMarker + labels?: ConflictedMarker + metrics?: string[] +} + +export const rangesOverlap = (one: PortRange, other: PortRange): boolean => one.from <= other.to && other.from <= one.to +export const rangesAreEqual = (one: PortRange, other: PortRange): boolean => + one.from === other.from && one.to === other.to + +const appendConflict = (conflicts: string[], oneId: string, otherId: string): string[] => { + if (!conflicts) { + return [oneId, otherId] + } + + if (!conflicts.includes(oneId)) { + conflicts.push(oneId) + } + + if (!conflicts.includes(otherId)) { + conflicts.push(otherId) + } + + return conflicts +} + +const appendUniqueItemConflicts = ( + conflicts: ConflictedUniqueItem[], + oneId: string, + otherId: string, + keys: string[], +): ConflictedUniqueItem[] => { + if (!conflicts) { + return keys.map(it => ({ + key: it, + configIds: [oneId, otherId], + })) + } + + keys.forEach(key => { + let conflict = conflicts.find(it => it.key === key) + if (!conflict) { + conflict = { + key, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendPortConflicts = ( + conflicts: ConflictedPort[], + oneId: string, + otherId: string, + internalPorts: number[], +): ConflictedPort[] => { + if (!conflicts) { + return internalPorts.map(it => ({ + internal: it, + configIds: [oneId, otherId], + })) + } + + internalPorts.forEach(internalPort => { + let conflict = conflicts.find(it => internalPort === it.internal) + if (!conflict) { + conflict = { + internal: internalPort, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendPortRangeConflicts = ( + conflicts: ConflictedPortRange[], + oneId: string, + otherId: string, + ranges: PortRange[], +): ConflictedPortRange[] => { + if (!conflicts) { + return ranges.map(it => ({ + range: it, + configIds: [oneId, otherId], + })) + } + + ranges.forEach(range => { + let conflict = conflicts.find(it => rangesAreEqual(it.range, range)) + if (!conflict) { + conflict = { + range, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendLogConflict = ( + conflicts: ConflictedLog, + oneId: string, + otherId: string, + keys: ConflictedLogKeys, +): ConflictedLog => { + if (!conflicts) { + conflicts = {} + } + + if (keys.driver) { + conflicts.driver = appendConflict(conflicts.driver, oneId, otherId) + } + + if (keys.options) { + conflicts.options = appendUniqueItemConflicts(conflicts.options, oneId, otherId, keys.options) + } + + return conflicts +} + +const appendResourceConfigConflict = ( + conflicts: ConflictedResoureConfig, + oneId: string, + otherId: string, + keys: ConflictedResoureConfigKeys, +): ConflictedResoureConfig => { + if (!conflicts) { + conflicts = {} + } + + if (keys.limits) { + conflicts.limits = appendConflict(conflicts.limits, oneId, otherId) + } + + if (keys.requests) { + conflicts.requests = appendConflict(conflicts.requests, oneId, otherId) + } + + return conflicts +} + +const appendMarkerConflict = ( + conflicts: ConflictedMarker, + oneId: string, + otherId: string, + keys: ConflictedMarkerKeys, +): ConflictedMarker => { + if (!conflicts) { + conflicts = {} + } + + if (keys.deployment) { + conflicts.deployment = appendUniqueItemConflicts(conflicts.deployment, oneId, otherId, keys.deployment) + } + + if (keys.ingress) { + conflicts.ingress = appendUniqueItemConflicts(conflicts.ingress, oneId, otherId, keys.ingress) + } + + if (keys.service) { + conflicts.service = appendUniqueItemConflicts(conflicts.service, oneId, otherId, keys.service) + } + + return conflicts +} + +const stringsConflict = (one: string, other: string): boolean => { + if (typeof one !== 'string' || typeof other !== 'string') { + // one of them are null or uninterpretable + return false + } + + return one !== other +} + +const booleansConflict = (one: boolean, other: boolean): boolean => { + if (typeof one !== 'boolean' || typeof other !== 'boolean') { + // one of them are null or uninterpretable + return false + } + + return one !== other +} + +const numbersConflict = (one: number, other: number): boolean => { + if (typeof one !== 'number' || typeof other !== 'number') { + // some of them are null or uninterpretable + return false + } + + return one !== other +} + +const objectsConflict = (one: object, other: object): boolean => { + if (typeof one !== 'object' || typeof other !== 'object') { + // some of them are null or uninterpretable + return false + } + + return JSON.stringify(one) !== JSON.stringify(other) +} + +// returns the conflicting keys +const uniqueKeyValuesConflict = (one: UniqueKeyValue[], other: UniqueKeyValue[]): string[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => { + const otherItem = other.find(it => it.key === item.key) + if (!otherItem) { + return false + } + + return item.value !== otherItem.value + }) + .map(it => it.key) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting internal ports +const portsConflict = (one: Port[], other: Port[]): number[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one.filter(item => other.find(it => it.internal === item.internal)).map(it => it.internal) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting internal ranges +const portRangesConflict = (one: ContainerPortRange[], other: ContainerPortRange[]): PortRange[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => + other.find(it => rangesOverlap(item.internal, it.internal) || rangesOverlap(item.external, item.internal)), + ) + .map(it => it.internal) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting paths +const volumesConflict = (one: Volume[], other: Volume[]): string[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => { + const otherItem = other.find(it => it.path === item.path) + + return objectsConflict(item, otherItem) + }) + .map(it => it.path) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +const logsConflict = (one: Log, other: Log): ConflictedLogKeys | null => { + if (!one || !other) { + return null + } + + const driver = stringsConflict(one.driver, other.driver) + const options = uniqueKeyValuesConflict(one.options, other.options) + + const conflicts: ConflictedLogKeys = {} + + if (driver) { + conflicts.driver = driver + } + + if (options) { + conflicts.options = options + } + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const resoureConfigsConflict = (one: ResourceConfig, other: ResourceConfig): ConflictedResoureConfigKeys | null => { + if (!one || !other) { + return null + } + + const conflicts: ConflictedResoureConfigKeys = { + limits: objectsConflict(one.limits, other.limits), + requests: objectsConflict(one.requests, other.requests), + } + + if (!Object.values(conflicts).find(it => it)) { + // no conflicts + return null + } + + return conflicts +} + +const markersConflict = (one: Marker, other: Marker): ConflictedMarkerKeys | null => { + if (!one || !other) { + return null + } + + const deployment = uniqueKeyValuesConflict(one.deployment, other.deployment) + const ingress = uniqueKeyValuesConflict(one.ingress, other.ingress) + const service = uniqueKeyValuesConflict(one.service, other.service) + + const conflicts: ConflictedMarkerKeys = {} + + if (deployment) { + conflicts.deployment = deployment + } + + if (ingress) { + conflicts.ingress = ingress + } + + if (service) { + conflicts.service = service + } + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const collectConflicts = ( + conflicts: ConflictedContainerConfigData, + one: ContainerConfigDataWithId, + other: ContainerConfigDataWithId, +) => { + const checkStringConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as string + const otherValue = other[key] as string + + if (stringsConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkUniqueKeyValuesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as UniqueKeyValue[] + const otherValue = other[key] as UniqueKeyValue[] + + const uniqueKeyValueConflicts = uniqueKeyValuesConflict(oneValue, otherValue) + if (uniqueKeyValueConflicts) { + conflicts[key] = appendUniqueItemConflicts(conflicts[key], one.id, other.id, uniqueKeyValueConflicts) + } + } + + const checkBooleanConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as boolean + const otherValue = other[key] as boolean + + if (booleansConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkNumberConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as number + const otherValue = other[key] as number + + if (numbersConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkObjectConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as object + const otherValue = other[key] as object + + if (objectsConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkPortsConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Port[] + const otherValue = other[key] as Port[] + + const portsConflicts = portsConflict(oneValue, otherValue) + if (portsConflicts) { + conflicts[key] = appendPortConflicts(conflicts[key], one.id, other.id, portsConflicts) + } + } + + const checkPortRangesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as ContainerPortRange[] + const otherValue = other[key] as ContainerPortRange[] + + const portRangesConflicts = portRangesConflict(oneValue, otherValue) + if (portRangesConflicts) { + conflicts[key] = appendPortRangeConflicts(conflicts[key], one.id, other.id, portRangesConflicts) + } + } + + const checkVolumesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Volume[] + const otherValue = other[key] as Volume[] + + const volumeConflicts = volumesConflict(oneValue, otherValue) + if (volumeConflicts) { + conflicts[key] = appendUniqueItemConflicts(conflicts[key], one.id, other.id, volumeConflicts) + } + } + + const checkStorageConflict = () => { + if (objectsConflict(one, other)) { + conflicts.storage = appendConflict(conflicts.storage, one.id, other.id) + } + } + + const checkLogsConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Log + const otherValue = other[key] as Log + + const logConflicts = logsConflict(oneValue, otherValue) + if (logConflicts) { + conflicts[key] = appendLogConflict(conflicts[key], one.id, other.id, logConflicts) + } + } + + const checkResourceConfigConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as ResourceConfig + const otherValue = other[key] as ResourceConfig + + const resourceConfigConflicts = resoureConfigsConflict(oneValue, otherValue) + if (resourceConfigConflicts) { + conflicts[key] = appendResourceConfigConflict(conflicts[key], one.id, other.id, resourceConfigConflicts) + } + } + + const checkMarkerConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Marker + const otherValue = other[key] as Marker + + const markerConflicts = markersConflict(oneValue, otherValue) + if (markerConflicts) { + conflicts[key] = appendMarkerConflict(conflicts[key], one.id, other.id, markerConflicts) + } + } + + // common + checkStringConflict('name') + checkUniqueKeyValuesConflict('environment') + // 'secrets' are keys only so duplicates are allowed + checkObjectConflict('routing') + checkStringConflict('expose') + checkNumberConflict('user') + checkStringConflict('workingDirectory') + checkBooleanConflict('tty') + checkObjectConflict('configContainer') + checkPortsConflict('ports') + checkPortRangesConflict('portRanges') + checkVolumesConflict('volumes') + // 'commands' are keys only so duplicates are allowed + // 'args' are keys only so duplicates are allowed + checkObjectConflict('initContainers') // TODO (@m8vago) compare them correctly after the init container rework + checkUniqueKeyValuesConflict('capabilities') + checkStorageConflict() + + // dagent + checkLogsConflict('logConfig') + checkStringConflict('restartPolicy') + checkStringConflict('networkMode') + // 'networks' are keys only so duplicates are allowed + checkUniqueKeyValuesConflict('dockerLabels') + checkStringConflict('expectedState') + + // crane + checkStringConflict('deploymentStrategy') + // 'customHeaders' are keys only so duplicates are allowed + checkBooleanConflict('proxyHeaders') + checkBooleanConflict('useLoadBalancer') + checkUniqueKeyValuesConflict('extraLBAnnotations') + checkObjectConflict('healthCheckConfig') + checkResourceConfigConflict('resourceConfig') + checkMarkerConflict('annotations') + checkMarkerConflict('labels') + checkObjectConflict('metrics') + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +type ContainerConfigDataProperty = keyof ContainerConfigData +export const checkForConflicts = ( + configs: ContainerConfigDataWithId[], + definedKeys: ContainerConfigDataProperty[] = [], +): ConflictedContainerConfigData | null => { + configs = configs.map(conf => { + const newConf: ContainerConfigDataWithId = { + ...conf, + } + + Object.keys(conf).forEach(it => { + const prop = it as ContainerConfigDataProperty + if (!definedKeys.includes(prop)) { + return + } + + delete newConf[prop] + }) + + return newConf + }) + + const conflicts: ConflictedContainerConfigData = {} + + configs.forEach(one => { + const others = configs.filter(it => it !== one) + + others.forEach(other => collectConflicts(conflicts, one, other)) + }) + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const UNINTERESTED_KEYS = ['id', 'type', 'updatedAt', 'updatedBy', 'secrets'] +export const getConflictsForConcreteConfig = ( + configs: ContainerConfigDataWithId[], + concreteConfig: ConcreteContainerConfigData, +): ConflictedContainerConfigData | null => + checkForConflicts( + configs, + Object.entries(concreteConfig) + .filter(entry => { + const [key, value] = entry + if (UNINTERESTED_KEYS.includes(key)) { + return false + } + + return typeof value !== 'undefined' && value !== null + }) + .map(entry => { + const [key] = entry + return key + }) as ContainerConfigDataProperty[], + ) diff --git a/web/crux-ui/src/models/container-errors.ts b/web/crux-ui/src/models/container-errors.ts new file mode 100644 index 0000000000..6fe17da377 --- /dev/null +++ b/web/crux-ui/src/models/container-errors.ts @@ -0,0 +1,194 @@ +import { Translate } from 'next-translate' +import { PortRange } from './container' +import { + ConflictedContainerConfigData, + ConflictedMarker, + ConflictedPort, + ConflictedPortRange, + ConflictedUniqueItem, +} from './container-conflict' + +export type UniqueItemErrors = Record +export type PortErrors = Record + +// from-to string keys +export type PortRangeErrors = Record + +export type LogError = { + driver?: string + options?: UniqueItemErrors +} + +export type ResourceConfigError = { + limits?: string + requests?: string +} + +export type MarkerError = { + service?: UniqueItemErrors + deployment?: UniqueItemErrors + ingress?: UniqueItemErrors +} + +// config ids where the given property is present +export type ContainerConfigErrors = { + // common + name?: string + environment?: UniqueItemErrors + routing?: string + expose?: string + user?: string + workingDirectory?: string + tty?: string + configContainer?: string + ports?: PortErrors + portRanges?: PortRangeErrors + volumes?: UniqueItemErrors + initContainers?: string + capabilities?: UniqueItemErrors + storage?: string + + // dagent + logConfig?: string + restartPolicy?: string + networkMode?: string + dockerLabels?: UniqueItemErrors + expectedState?: string + + // crane + deploymentStrategy?: string + proxyHeaders?: string + useLoadBalancer?: string + extraLBAnnotations?: UniqueItemErrors + healthCheckConfig?: string + resourceConfig?: string + annotations?: MarkerError + labels?: MarkerError + metrics?: string +} + +export const portRangeToString = (range: PortRange) => `${range.from}-${range.to}` +export const portRangeFromString = (range: string): PortRange => { + const [from, to] = range.split('-') + return { + from: Number.parseInt(from, 10), + to: Number.parseInt(to, 10), + } +} + +export const conflictsToError = ( + t: Translate, + configNames: Record, + conflicts: ConflictedContainerConfigData, +): ContainerConfigErrors | null => { + if (!conflicts) { + return null + } + + const errors: ContainerConfigErrors = {} + + const idsToConfigNames = (ids: string[]): string => + ids + .map(it => configNames[it]) + .filter(it => !!it) + .join(', ') + + const uniqueItemConflictsToError = (conflict: ConflictedUniqueItem[]): UniqueItemErrors => + conflict.reduce((result, it) => { + result[it.key] = t('container:errors.ambiguousInConfigs', { configs: idsToConfigNames(it.configIds) }) + return result + }, {}) + + const checkStringError = (key: keyof ConflictedContainerConfigData) => { + if (key in conflicts) { + const conflict = conflicts[key] as string[] + errors[key as string] = t('container:errors.ambiguousKeyInConfigs', { key, configs: idsToConfigNames(conflict) }) + } + } + + const checkUniqueErrors = (key: keyof ConflictedContainerConfigData) => { + if (key in conflicts) { + const conflict = conflicts[key] as ConflictedUniqueItem[] + const itemConflicts = uniqueItemConflictsToError(conflict) + + errors[key as string] = itemConflicts + } + } + + const checkPortErrors = (key: keyof ConflictedContainerConfigData) => { + if (key in conflicts) { + const conflict = conflicts[key] as ConflictedPort[] + const portConflicts = conflict.reduce((result, it) => { + result[it.internal] = t('container:errors.ambiguousInConfigs', { configs: idsToConfigNames(it.configIds) }) + return result + }, {}) + errors[key as string] = portConflicts + } + } + + const checkPortRangeErrors = (key: keyof ConflictedContainerConfigData) => { + if (key in conflicts) { + const conflict = conflicts[key] as ConflictedPortRange[] + const portRangeConflicts = conflict.reduce((result, it) => { + const rangeKey = portRangeToString(it.range) + result[rangeKey] = t('container:errors.ambiguousInConfigs', { configs: idsToConfigNames(it.configIds) }) + return result + }, {}) + errors[key as string] = portRangeConflicts + } + } + + const checkMarkerErrors = (key: keyof ConflictedContainerConfigData) => { + if (key in conflicts) { + const conflict = conflicts[key] as ConflictedMarker + + const err: MarkerError = { + service: conflict.deployment ? uniqueItemConflictsToError(conflict.service) : null, + deployment: conflict.deployment ? uniqueItemConflictsToError(conflict.deployment) : null, + ingress: conflict.deployment ? uniqueItemConflictsToError(conflict.ingress) : null, + } + + errors[key as string] = err + } + } + + checkStringError('name') + checkUniqueErrors('environment') + + checkStringError('routing') + checkStringError('expose') + checkStringError('user') + checkStringError('workingDirectory') + checkStringError('tty') + checkStringError('configContainer') + checkPortErrors('ports') + checkPortRangeErrors('portRanges') + checkUniqueErrors('volumes') + checkStringError('initContainers') + checkStringError('capabilities') + checkStringError('storage') + + // dagent + checkStringError('logConfig') + checkStringError('restartPolicy') + checkStringError('networkMode') + checkUniqueErrors('dockerLabels') + checkStringError('expectedState') + + // crane + checkStringError('deploymentStrategy') + checkStringError('proxyHeaders') + checkStringError('useLoadBalancer') + checkUniqueErrors('extraLBAnnotations') + checkStringError('healthCheckConfig') + checkStringError('resourceConfig') + checkMarkerErrors('annotations') + checkMarkerErrors('labels') + checkStringError('metrics') + + if (Object.keys(errors).length < 1) { + return null + } + + return errors +} diff --git a/web/crux-ui/src/models/container-merge.ts b/web/crux-ui/src/models/container-merge.ts new file mode 100644 index 0000000000..5d7052faa1 --- /dev/null +++ b/web/crux-ui/src/models/container-merge.ts @@ -0,0 +1,240 @@ +import { + ConcreteContainerConfigData, + ContainerConfigData, + ContainerPortRange, + Marker, + Port, + UniqueKey, + UniqueSecretKey, + UniqueSecretKeyValue, + Volume, +} from './container' +import { rangesOverlap } from './container-conflict' + +const mergeNumber = (strong: number, weak: number): number => { + if (typeof strong === 'number') { + return strong + } + + if (typeof weak === 'number') { + return weak + } + + return null +} + +const mergeBoolean = (strong: boolean, weak: boolean): boolean => { + if (typeof strong === 'boolean') { + return strong + } + + if (typeof weak === 'boolean') { + return weak + } + + return null +} + +export const mergeMarkers = (strong: Marker, weak: Marker): Marker => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + return { + deployment: strong.deployment ?? weak.deployment ?? [], + ingress: strong.ingress ?? weak.ingress ?? [], + service: strong.service ?? weak.service ?? [], + } +} + +const mergeSecretKeys = (one: UniqueSecretKey[], other: UniqueSecretKey[]): UniqueSecretKey[] => { + if (!one) { + return other + } + + if (!other) { + return one + } + + return [...one, ...other.filter(it => !one.includes(it))] +} + +export const mergeSecrets = (strong: UniqueSecretKeyValue[], weak: UniqueSecretKey[]): UniqueSecretKeyValue[] => { + weak = weak ?? [] + strong = strong ?? [] + + const overriddenIds: Set = new Set(strong?.map(it => it.id)) + + const missing: UniqueSecretKeyValue[] = weak + .filter(it => !overriddenIds.has(it.id)) + .map(it => ({ + ...it, + value: '', + encrypted: false, + publicKey: null, + })) + + return [...missing, ...strong] +} + +export const mergeConfigs = (strong: ContainerConfigData, weak: ContainerConfigData): ContainerConfigData => ({ + // common + name: strong.name ?? weak.name, + environment: strong.environment ?? weak.environment, + secrets: mergeSecretKeys(strong.secrets, weak.secrets), + user: mergeNumber(strong.user, weak.user), + workingDirectory: strong.workingDirectory ?? weak.workingDirectory, + tty: mergeBoolean(strong.tty, weak.tty), + portRanges: strong.portRanges ?? weak.portRanges, + args: strong.args ?? weak.args, + commands: strong.commands ?? weak.commands, + expose: strong.expose ?? weak.expose, + configContainer: strong.configContainer ?? weak.configContainer, + routing: strong.routing ?? weak.routing, + volumes: strong.volumes ?? weak.volumes, + initContainers: strong.initContainers ?? weak.initContainers, + capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing + ports: strong.ports ?? weak.ports, + storage: strong.storage ?? weak.storage, + + // crane + customHeaders: strong.customHeaders ?? weak.customHeaders, + proxyHeaders: mergeBoolean(strong.proxyHeaders, weak.proxyHeaders), + extraLBAnnotations: strong.extraLBAnnotations ?? weak.extraLBAnnotations, + healthCheckConfig: strong.healthCheckConfig ?? weak.healthCheckConfig, + resourceConfig: strong.resourceConfig ?? weak.resourceConfig, + useLoadBalancer: mergeBoolean(strong.useLoadBalancer, weak.useLoadBalancer), + deploymentStrategy: strong.deploymentStrategy ?? weak.deploymentStrategy, + labels: mergeMarkers(strong.labels, weak.labels), + annotations: mergeMarkers(strong.annotations, weak.annotations), + metrics: strong.metrics ?? weak.metrics, + + // dagent + logConfig: strong.logConfig ?? weak.logConfig, + networkMode: strong.networkMode ?? weak.networkMode, + restartPolicy: strong.restartPolicy ?? weak.restartPolicy, + networks: strong.networks ?? weak.networks, + dockerLabels: strong.dockerLabels ?? weak.dockerLabels, + expectedState: strong.expectedState ?? weak.expectedState, +}) + +export const squashConfigs = (configs: ContainerConfigData[]): ContainerConfigData => + configs.reduce((result, conf) => mergeConfigs(conf, result), {} as ContainerConfigData) + +// this assumes that the concrete config takes care of any conflict between the other configs +export const mergeConfigsWithConcreteConfig = ( + configs: ContainerConfigData[], + concrete: ConcreteContainerConfigData, +): ConcreteContainerConfigData => { + const squashed = squashConfigs(configs.filter(it => !!it)) + concrete = concrete ?? {} + + const baseConfig = mergeConfigs(concrete, squashed) + + return { + ...baseConfig, + secrets: mergeSecrets(concrete.secrets, squashed.secrets), + } +} + +const mergeUniqueKeys = (strong: T[], weak: T[]): T[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.key === w.key)) + return [...strong, ...missing] +} + +const mergePorts = (strong: Port[], weak: Port[]): Port[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.internal === w.internal)) + return [...strong, ...missing] +} + +const mergePortRanges = (strong: ContainerPortRange[], weak: ContainerPortRange[]): ContainerPortRange[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter( + w => !strong.find(it => rangesOverlap(w.internal, it.internal) || rangesOverlap(w.external, it.external)), + ) + return [...strong, ...missing] +} + +const mergeVolumes = (strong: Volume[], weak: Volume[]): Volume[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.path === w.path || it.name === w.path)) + return [...strong, ...missing] +} + +export const mergeInstanceConfigWithDeploymentConfig = ( + deployment: ConcreteContainerConfigData, + instance: ConcreteContainerConfigData, +): ConcreteContainerConfigData => ({ + // common + name: instance.name ?? deployment.name ?? null, + environment: mergeUniqueKeys(instance.environment, deployment.environment), + secrets: mergeUniqueKeys(instance.secrets, deployment.secrets), + user: mergeNumber(instance.user, deployment.user), + workingDirectory: instance.workingDirectory ?? deployment.workingDirectory ?? null, + tty: mergeBoolean(instance.tty, deployment.tty), + ports: mergePorts(instance.ports, deployment.ports), + portRanges: mergePortRanges(instance.portRanges, deployment.portRanges), + args: mergeUniqueKeys(instance.args, deployment.args), + commands: mergeUniqueKeys(instance.commands, deployment.commands), + expose: instance.expose ?? deployment.expose ?? null, + configContainer: instance.configContainer ?? deployment.configContainer ?? null, + routing: instance.routing ?? deployment.routing ?? null, + volumes: mergeVolumes(instance.volumes, deployment.volumes), + initContainers: instance.initContainers ?? deployment.initContainers ?? null, // TODO (@m8vago): merge them correctly after the init container rework + capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing + storage: instance.storage ?? deployment.storage, + + // crane + customHeaders: mergeUniqueKeys(deployment.customHeaders, instance.customHeaders), + proxyHeaders: mergeBoolean(instance.proxyHeaders, deployment.proxyHeaders), + extraLBAnnotations: mergeUniqueKeys(instance.extraLBAnnotations, deployment.extraLBAnnotations), + healthCheckConfig: instance.healthCheckConfig ?? deployment.healthCheckConfig ?? null, + resourceConfig: instance.resourceConfig ?? deployment.resourceConfig ?? null, + useLoadBalancer: mergeBoolean(instance.useLoadBalancer, deployment.useLoadBalancer), + deploymentStrategy: instance.deploymentStrategy ?? deployment.deploymentStrategy ?? null, + labels: mergeMarkers(instance.labels, deployment.labels), + annotations: mergeMarkers(instance.annotations, deployment.annotations), + metrics: instance.metrics ?? deployment.metrics ?? null, + + // dagent + logConfig: instance.logConfig ?? deployment.logConfig ?? null, + networkMode: instance.networkMode ?? deployment.networkMode ?? null, + restartPolicy: instance.restartPolicy ?? deployment.restartPolicy ?? null, + networks: mergeUniqueKeys(instance.networks, deployment.networks), + dockerLabels: mergeUniqueKeys(instance.dockerLabels, deployment.dockerLabels), + expectedState: instance.expectedState ?? deployment.expectedState ?? null, +}) diff --git a/web/crux-ui/src/models/container.ts b/web/crux-ui/src/models/container.ts index 6a86b917e0..7042ee66f8 100644 --- a/web/crux-ui/src/models/container.ts +++ b/web/crux-ui/src/models/container.ts @@ -1,4 +1,5 @@ import { v4 as uuid } from 'uuid' +import { imageName } from './registry' export const CONTAINER_STATE_VALUES = ['running', 'waiting', 'exited', 'removed'] as const export type ContainerState = (typeof CONTAINER_STATE_VALUES)[number] @@ -57,7 +58,7 @@ export type UniqueSecretKeyValue = UniqueSecretKey & encrypted: boolean } -export type ContainerConfigPort = { +export type Port = { id: string internal: number external?: number @@ -68,7 +69,7 @@ export type PortRange = { to: number } -export type ContainerConfigPortRange = { +export type ContainerPortRange = { id: string internal: PortRange external: PortRange @@ -99,7 +100,7 @@ export type ContainerConfigRouting = { port?: number } -export type ContainerConfigVolume = { +export type Volume = { id: string name: string path: string @@ -125,7 +126,7 @@ export const CONTAINER_LOG_DRIVER_VALUES = [ ] as const export type ContainerLogDriverType = (typeof CONTAINER_LOG_DRIVER_VALUES)[number] -export type ContainerConfigLog = { +export type Log = { driver: ContainerLogDriverType options: UniqueKeyValue[] } @@ -142,7 +143,7 @@ export type ContainerConfigResource = { memory?: string } -export type ContainerConfigResourceConfig = { +export type ResourceConfig = { limits?: ContainerConfigResource requests?: ContainerConfigResource } @@ -195,79 +196,126 @@ export type ExpectedContainerState = { exitCode?: number } +export type ContainerConfigType = 'image' | 'instance' | 'deployment' | 'config-bundle' +export type ContainerConfigSectionType = 'base' | 'concrete' + +export type ContainerConfig = (ContainerConfigData | ConcreteContainerConfigData) & { + id: string + type: ContainerConfigType +} + +export type ContainerConfigDataWithId = ContainerConfig + export type ContainerConfigData = { // common - name: string + name?: string environment?: UniqueKeyValue[] secrets?: UniqueSecretKey[] routing?: ContainerConfigRouting - expose: ContainerConfigExposeStrategy + expose?: ContainerConfigExposeStrategy user?: number workingDirectory?: string - tty: boolean + tty?: boolean configContainer?: ContainerConfigContainer - ports?: ContainerConfigPort[] - portRanges?: ContainerConfigPortRange[] - volumes?: ContainerConfigVolume[] + ports?: Port[] + portRanges?: ContainerPortRange[] + volumes?: Volume[] commands?: UniqueKey[] args?: UniqueKey[] initContainers?: InitContainer[] - capabilities: UniqueKeyValue[] + capabilities?: UniqueKeyValue[] storage?: ContainerStorage // dagent - logConfig?: ContainerConfigLog - restartPolicy: ContainerRestartPolicyType - networkMode: ContainerNetworkMode + logConfig?: Log + restartPolicy?: ContainerRestartPolicyType + networkMode?: ContainerNetworkMode networks?: UniqueKey[] dockerLabels?: UniqueKeyValue[] expectedState?: ExpectedContainerState // crane - deploymentStrategy: ContainerDeploymentStrategyType + deploymentStrategy?: ContainerDeploymentStrategyType customHeaders?: UniqueKey[] - proxyHeaders: boolean - useLoadBalancer: boolean + proxyHeaders?: boolean + useLoadBalancer?: boolean extraLBAnnotations?: UniqueKeyValue[] healthCheckConfig?: ContainerConfigHealthCheck - resourceConfig?: ContainerConfigResourceConfig + resourceConfig?: ResourceConfig annotations?: Marker labels?: Marker metrics?: Metrics } -type DagentSpecificConfig = - | 'logConfig' - | 'restartPolicy' - | 'networkMode' - | 'networks' - | 'dockerLabels' - | 'expectedState' -type CraneSpecificConfig = - | 'deploymentStrategy' - | 'customHeaders' - | 'proxyHeaders' - | 'useLoadBalancer' - | 'extraLBAnnotations' - | 'healthCheckConfig' - | 'resourceConfig' - | 'labels' - | 'annotations' - | 'metrics' - -export type DagentConfigDetails = Pick -export type CraneConfigDetails = Pick -export type CommonConfigDetails = Omit - -export type InstanceDagentConfigDetails = Pick -export type InstanceCraneConfigDetails = Pick -export type InstanceCommonConfigDetails = Omit - -export type MergedContainerConfigData = Omit & { - secrets: UniqueSecretKeyValue[] -} - -export type InstanceContainerConfigData = Partial +export const COMMON_CONFIG_KEYS = [ + 'name', + 'environment', + 'secrets', + 'routing', + 'expose', + 'user', + 'tty', + 'workingDirectory', + 'configContainer', + 'ports', + 'portRanges', + 'volumes', + 'commands', + 'args', + 'initContainers', + 'storage', +] as const + +export const CRANE_CONFIG_KEYS = [ + 'deploymentStrategy', + 'customHeaders', + 'proxyHeaders', + 'useLoadBalancer', + 'extraLBAnnotations', + 'healthCheckConfig', + 'resourceConfig', + 'labels', + 'annotations', + 'metrics', +] as const + +export const DAGENT_CONFIG_KEYS = [ + 'logConfig', + 'restartPolicy', + 'networkMode', + 'networks', + 'dockerLabels', + 'expectedState', +] as const + +export const CONTAINER_CONFIG_KEYS = [...COMMON_CONFIG_KEYS, ...CRANE_CONFIG_KEYS, ...DAGENT_CONFIG_KEYS] as const + +export type CommonConfigKey = (typeof COMMON_CONFIG_KEYS)[number] +export type CraneConfigKey = (typeof CRANE_CONFIG_KEYS)[number] +export type DagentConfigKey = (typeof DAGENT_CONFIG_KEYS)[number] +export type ContainerConfigKey = (typeof CONTAINER_CONFIG_KEYS)[number] + +export type ConcreteContainerConfigData = Omit & { + secrets?: UniqueSecretKeyValue[] +} + +export type ConcreteContainerConfig = ConcreteContainerConfigData & { + id: string + type: ContainerConfigType +} + +export const CRANE_CONFIG_FILTER_VALUES = CRANE_CONFIG_KEYS.filter(it => it !== 'extraLBAnnotations') + +export type ContainerConfigFilterType = 'all' | 'common' | 'dagent' | 'crane' + +export const filterContains = ( + filter: CommonConfigKey | CraneConfigKey | DagentConfigKey, + filters: ContainerConfigKey[], +): boolean => filters.includes(filter) + +export const filterEmpty = (filterValues: string[], filters: ContainerConfigKey[]): boolean => + filterValues.filter(x => filters.includes(x as ContainerConfigKey)).length > 0 + export type JsonInitContainer = { name: string image: string @@ -290,9 +338,9 @@ export type JsonMarker = { } export type JsonInitContainerVolumeLink = Omit -export type JsonContainerConfigPortRange = Omit -export type JsonContainerConfigPort = Omit -export type JsonContainerConfigVolume = Omit +export type JsonContainerConfigPortRange = Omit +export type JsonContainerConfigPort = Omit +export type JsonContainerConfigVolume = Omit export type JsonContainerConfigSecretKey = Omit export type JsonContainerConfig = { @@ -303,6 +351,7 @@ export type JsonContainerConfig = { routing?: ContainerConfigRouting expose?: ContainerConfigExposeStrategy user?: number + workingDirectory?: string tty?: boolean configContainer?: ContainerConfigContainer ports?: JsonContainerConfigPort[] @@ -320,7 +369,7 @@ export type JsonContainerConfig = { restartPolicy?: ContainerRestartPolicyType networkMode?: ContainerNetworkMode networks?: string[] - dockerLabels: JsonKeyValue + dockerLabels?: JsonKeyValue // crane deploymentStrategy?: ContainerDeploymentStrategyType @@ -329,109 +378,47 @@ export type JsonContainerConfig = { useLoadBalancer?: boolean extraLBAnnotations?: JsonKeyValue healthCheckConfig?: ContainerConfigHealthCheck - resourceConfig?: ContainerConfigResourceConfig - annotations: JsonMarker - labels: JsonMarker + resourceConfig?: ResourceConfig + annotations?: JsonMarker + labels?: JsonMarker } -export type InstanceJsonContainerConfig = Omit - -const mergeSecrets = ( - imageSecrets: UniqueSecretKey[], - instanceSecrets: UniqueSecretKeyValue[], -): UniqueSecretKeyValue[] => { - imageSecrets = imageSecrets ?? [] - instanceSecrets = instanceSecrets ?? [] +export type ConcreteJsonContainerConfig = Omit - const overriddenIds: Set = new Set(instanceSecrets?.map(it => it.id)) +export const stringResettable = (base: string, concrete: string): boolean => { + if (!concrete) { + return false + } - const missing: UniqueSecretKeyValue[] = imageSecrets - .filter(it => !overriddenIds.has(it.id)) - .map(it => ({ - ...it, - value: '', - encrypted: false, - publicKey: null, - })) + if (!base) { + return true + } - return [...missing, ...instanceSecrets] + return base !== concrete } -const mergeMarker = (image: Marker, instance: Marker): Marker => { - if (!instance) { - return image +export const numberResettable = (base: number, concrete: number): boolean => { + if (typeof concrete !== 'number') { + return false } - if (!image) { - return null + if (typeof base !== 'number') { + return true } - return { - deployment: instance.deployment ?? image.deployment, - ingress: instance.ingress ?? image.ingress, - service: instance.service ?? image.service, - } + return base !== concrete } -const mergeMetrics = (image: Metrics, instance: Metrics): Metrics => { - if (!instance) { - return image?.enabled ? image : null +export const booleanResettable = (base: boolean, concrete: boolean): boolean => { + if (typeof concrete !== 'boolean') { + return false } - return instance -} - -export const mergeConfigs = ( - image: ContainerConfigData, - instance: InstanceContainerConfigData, -): MergedContainerConfigData => { - instance = instance ?? {} - - return { - name: instance.name ?? image.name, - environment: instance.environment ?? image.environment, - secrets: mergeSecrets(image.secrets, instance.secrets), - ports: instance.ports ?? image.ports, - user: instance.user ?? image.user, - workingDirectory: instance.workingDirectory ?? image.workingDirectory, - tty: instance.tty ?? image.tty, - portRanges: instance.portRanges ?? image.portRanges, - args: instance.args ?? image.args, - commands: instance.commands ?? image.commands, - expose: instance.expose ?? image.expose, - configContainer: instance.configContainer ?? image.configContainer, - routing: instance.routing ?? image.routing, - volumes: instance.volumes ?? image.volumes, - initContainers: instance.initContainers ?? image.initContainers, - capabilities: null, - storage: instance.storage ?? image.storage, - - // crane - customHeaders: instance.customHeaders ?? image.customHeaders, - proxyHeaders: instance.proxyHeaders ?? image.proxyHeaders, - extraLBAnnotations: instance.extraLBAnnotations ?? image.extraLBAnnotations, - healthCheckConfig: instance.healthCheckConfig ?? image.healthCheckConfig, - resourceConfig: instance.resourceConfig ?? image.resourceConfig, - useLoadBalancer: instance.useLoadBalancer ?? image.useLoadBalancer, - deploymentStrategy: instance.deploymentStrategy ?? instance.deploymentStrategy ?? 'recreate', - labels: mergeMarker(image.labels, instance.labels), - annotations: mergeMarker(image.annotations, instance.annotations), - metrics: mergeMetrics(image.metrics, instance.metrics), - - // dagent - logConfig: instance.logConfig ?? image.logConfig, - networkMode: instance.networkMode ?? image.networkMode ?? 'none', - restartPolicy: instance.restartPolicy ?? image.restartPolicy ?? 'unlessStopped', - networks: instance.networks ?? image.networks, - dockerLabels: instance.dockerLabels ?? image.dockerLabels, - expectedState: - !!image.expectedState || !!instance.expectedState - ? { - ...image.expectedState, - ...instance.expectedState, - } - : null, + if (typeof base !== 'boolean') { + return true } + + return base !== concrete } const keyValueArrayToJson = (list: UniqueKeyValue[]): JsonKeyValue => @@ -446,62 +433,71 @@ const removeId = (item: T): Omit => { return newItem } -export const imageConfigToJsonContainerConfig = (config: Partial): JsonContainerConfig => { - const jsonConfig = { - ...config, - commands: keyArrayToJson(config.commands), - args: keyArrayToJson(config.args), - networks: keyArrayToJson(config.networks), - customHeaders: keyArrayToJson(config.customHeaders), - extraLBAnnotations: keyValueArrayToJson(config.extraLBAnnotations), - environment: keyValueArrayToJson(config.environment), - capabilities: keyValueArrayToJson(config.capabilities), - secrets: config.secrets?.map(it => ({ key: it.key, required: it.required })), - portRanges: config.portRanges?.map(it => removeId(it)), - ports: config.ports?.map(it => removeId(it)), - storage: config.storage, - logConfig: config.logConfig - ? { - ...config.logConfig, - options: keyValueArrayToJson(config.logConfig?.options), - } - : null, - initContainers: config.initContainers?.map(container => ({ - ...removeId(container), - command: keyArrayToJson(container.command), - args: keyArrayToJson(container.args), - environment: keyValueArrayToJson(container.environment), - volumes: container.volumes?.map(vit => removeId(vit)), - })), - volumes: config.volumes?.map(it => removeId(it)), - dockerLabels: keyValueArrayToJson(config.dockerLabels), - annotations: config.annotations - ? { - deployment: keyValueArrayToJson(config.annotations.deployment), - service: keyValueArrayToJson(config.annotations.service), - ingress: keyValueArrayToJson(config.annotations.ingress), - } - : null, - labels: config.labels - ? { - deployment: keyValueArrayToJson(config.labels.deployment), - service: keyValueArrayToJson(config.labels.service), - ingress: keyValueArrayToJson(config.labels.ingress), - } - : null, - } +export const containerConfigToJsonConfig = (config: ContainerConfigData): JsonContainerConfig => ({ + // common + name: config.name, + environment: keyValueArrayToJson(config.environment), + // secrets are ommited + routing: config.routing, + expose: config.expose, + user: config.user, + workingDirectory: config.workingDirectory, + tty: config.tty, + configContainer: config.configContainer, + ports: config.ports?.map(it => removeId(it)), + portRanges: config.portRanges?.map(it => removeId(it)), + volumes: config.volumes?.map(it => removeId(it)), + commands: keyArrayToJson(config.commands), + args: keyArrayToJson(config.args), + initContainers: config.initContainers?.map(container => ({ + ...removeId(container), + command: keyArrayToJson(container.command), + args: keyArrayToJson(container.args), + environment: keyValueArrayToJson(container.environment), + volumes: container.volumes?.map(vit => removeId(vit)), + })), + capabilities: keyValueArrayToJson(config.capabilities), + storage: config.storage, - const configObject = jsonConfig as any - delete configObject.id - delete configObject.imageId + // dagent + logConfig: config.logConfig + ? { + ...config.logConfig, + options: keyValueArrayToJson(config.logConfig?.options), + } + : null, + restartPolicy: config.restartPolicy, + networkMode: config.networkMode, + networks: keyArrayToJson(config.networks), + dockerLabels: keyValueArrayToJson(config.dockerLabels), + expectedState: config.expectedState, - return jsonConfig -} + // crane + deploymentStrategy: config.deploymentStrategy, + customHeaders: keyArrayToJson(config.customHeaders), + proxyHeaders: config.proxyHeaders, + useLoadBalancer: config.useLoadBalancer, + extraLBAnnotations: keyValueArrayToJson(config.extraLBAnnotations), + healthCheckConfig: config.healthCheckConfig, + resourceConfig: config.resourceConfig, + annotations: config.annotations + ? { + deployment: keyValueArrayToJson(config.annotations.deployment), + service: keyValueArrayToJson(config.annotations.service), + ingress: keyValueArrayToJson(config.annotations.ingress), + } + : null, + labels: config.labels + ? { + deployment: keyValueArrayToJson(config.labels.deployment), + service: keyValueArrayToJson(config.labels.service), + ingress: keyValueArrayToJson(config.labels.ingress), + } + : null, +}) -export const instanceConfigToJsonInstanceConfig = ( - config: InstanceContainerConfigData, -): InstanceJsonContainerConfig => { - const json = imageConfigToJsonContainerConfig(config) +export const concreteContainerConfigToJsonConfig = (config: ConcreteContainerConfig): ConcreteJsonContainerConfig => { + const json = containerConfigToJsonConfig(config) delete json.secrets @@ -664,11 +660,11 @@ const mergeInitContainersWithJson = (containers: InitContainer[], json: JsonInit return containers } -export const mergeJsonConfigToInstanceContainerConfig = ( - config: InstanceContainerConfigData, - json: InstanceJsonContainerConfig, -): InstanceContainerConfigData => { - const result: InstanceContainerConfigData = { +export const mergeJsonConfigToConcreteContainerConfig = ( + config: ConcreteContainerConfig, + json: ConcreteJsonContainerConfig, +): ConcreteContainerConfig => { + const result: ConcreteContainerConfig = { ...config, ...json, environment: mergeKeyValuesWithJson(config.environment, json.environment), @@ -728,19 +724,16 @@ export const mergeJsonConfigToInstanceContainerConfig = ( return result } -export const mergeJsonConfigToImageContainerConfig = ( - config: ContainerConfigData, - json: JsonContainerConfig, -): ContainerConfigData => { - const asInstanceConfig = { +export const mergeJsonWithContainerConfig = (config: ContainerConfig, json: JsonContainerConfig): ContainerConfig => { + const concreteConfig: ConcreteContainerConfig = { ...config, secrets: null, } - const instanceConf = mergeJsonConfigToInstanceContainerConfig(asInstanceConfig, json) + const mergedConf = mergeJsonConfigToConcreteContainerConfig(concreteConfig, json) return { ...config, - ...instanceConf, + ...mergedConf, secrets: mergeSecretsWithJson(config.secrets, json.secrets), } } @@ -799,6 +792,7 @@ export const containerPortsToString = (ports: ContainerPort[], truncateAfter: nu return result.join(', ') } +export const imageNameOfContainer = (container: Container): string => imageName(container.imageName, container.imageTag) export const containerPrefixNameOf = (id: ContainerIdentifier): string => !id.prefix ? id.name : `${id.prefix}-${id.name}` @@ -814,3 +808,11 @@ export const containerIsHidden = (it: Container) => { return serviceCategoryIsHidden(serviceCategory) || kubeNamespaceIsSystem(kubeNamespace) } + +export const containerConfigTypeToSectionType = (type: ContainerConfigType): ContainerConfigSectionType => { + if (type === 'instance' || type === 'deployment') { + return 'concrete' + } + + return 'base' +} diff --git a/web/crux-ui/src/models/deployment.ts b/web/crux-ui/src/models/deployment.ts index 1ae42fc703..bab857e667 100644 --- a/web/crux-ui/src/models/deployment.ts +++ b/web/crux-ui/src/models/deployment.ts @@ -1,7 +1,8 @@ import { Audit } from './audit' -import { DeploymentStatus, DyoApiError, slugify } from './common' -import { ContainerIdentifier, ContainerState, InstanceContainerConfigData, UniqueKeyValue } from './container' -import { ImageConfigProperty, ImageDeletedMessage } from './image' +import { DeploymentStatus, DyoApiError, PaginatedList, PaginationQuery, slugify } from './common' +import { ConfigBundleDetails } from './config-bundle' +import { ConcreteContainerConfig, ContainerState } from './container' +import { ImageDeletedMessage, VersionImage } from './image' import { Instance } from './instance' import { DyoNode } from './node' import { BasicProject, ProjectDetails } from './project' @@ -29,6 +30,15 @@ export type Deployment = { version: BasicVersion } +export type DeploymentQuery = PaginationQuery & { + nodeId?: string + filter?: string + status?: DeploymentStatus + configBundleId?: string +} + +export type DeploymentList = PaginatedList + export type DeploymentToken = { id: string name: string @@ -46,12 +56,15 @@ export type DeploymentTokenCreated = DeploymentToken & { curl: string } -export type DeploymentDetails = Deployment & { - environment: UniqueKeyValue[] - configBundleEnvironment: EnvironmentToConfigBundleNameMap +export type DeploymentWithConfig = Deployment & { + config: ConcreteContainerConfig publicKey?: string - configBundleIds?: string[] - token: DeploymentToken + configBundles: ConfigBundleDetails[] +} + +export type DeploymentDetails = DeploymentWithConfig & { + token?: DeploymentToken + lastTry: number instances: Instance[] } @@ -105,12 +118,11 @@ export type CreateDeployment = { note?: string | undefined } -export type PatchDeployment = { - id: string - prefix?: string +export type UpdateDeployment = { note?: string + prefix?: string protected?: boolean - environment?: UniqueKeyValue[] + configBundles?: string[] } export type CopyDeployment = { @@ -137,30 +149,10 @@ export type StartDeployment = { } // ws - -export const WS_TYPE_PATCH_DEPLOYMENT_ENV = 'patch-deployment-env' -export type PatchDeploymentEnvMessage = { - environment?: UniqueKeyValue[] - configBundleIds?: string[] -} - -export const WS_TYPE_DEPLOYMENT_ENV_UPDATED = 'deployment-env-updated' -export type DeploymentEnvUpdatedMessage = { - environment?: UniqueKeyValue[] - configBundleIds?: string[] - configBundleEnvironment: EnvironmentToConfigBundleNameMap -} - -export const WS_TYPE_PATCH_INSTANCE = 'patch-instance' -export type PatchInstanceMessage = { - instanceId: string - config?: Partial - resetSection?: ImageConfigProperty -} - -export const WS_TYPE_INSTANCE_UPDATED = 'instance-updated' -export type InstanceUpdatedMessage = InstanceContainerConfigData & { - instanceId: string +// TODO (@m8vago): move this to the container-config ws endpoint +export const WS_TYPE_DEPLOYMENT_BUNDLES_UPDATED = 'deployment-bundles-updated' +export type DeploymentBundlesUpdatedMessage = { + bundles: ConfigBundleDetails[] } export const WS_TYPE_GET_INSTANCE = 'get-instance' @@ -168,11 +160,19 @@ export type GetInstanceMessage = { id: string } -export const WS_TYPE_INSTANCE = 'instance' -export type InstanceMessage = Instance & {} - export const WS_TYPE_INSTANCES_ADDED = 'instances-added' -export type InstancesAddedMessage = Instance[] +type InstanceCreatedMessage = { + id: string + configId: string + image: VersionImage +} +export type InstancesAddedMessage = InstanceCreatedMessage[] + +export const WS_TYPE_INSTANCE_DELETED = 'instance-deleted' +export type InstanceDeletedMessage = { + instanceId: string + configId: string +} export type DeploymentEditEventMessage = InstancesAddedMessage | ImageDeletedMessage @@ -187,25 +187,6 @@ export type DeploymentEventMessage = DeploymentEvent export const WS_TYPE_DEPLOYMENT_FINISHED = 'deployment-finished' -export const WS_TYPE_GET_INSTANCE_SECRETS = 'get-instance-secrets' -export type GetInstanceSecretsMessage = { - id: string -} - -export type InstanceSecrets = { - container: ContainerIdentifier - - publicKey: string - - keys?: string[] -} - -export const WS_TYPE_INSTANCE_SECRETS = 'instance-secrets' -export type InstanceSecretsMessage = { - instanceId: string - keys: string[] -} - export const deploymentIsMutable = (status: DeploymentStatus, type: VersionType): boolean => { switch (status) { case 'preparing': @@ -247,3 +228,11 @@ export const lastDeploymentStatusOfEvents = (events: DeploymentEvent[]): Deploym export const deploymentHasError = (dto: DyoApiError): boolean => dto.error === 'rollingVersionDeployment' || dto.error === 'alreadyHavePreparing' + +export const instanceCreatedMessageToInstance = (it: InstanceCreatedMessage): Instance => ({ + ...it, + config: { + id: it.configId, + type: 'instance', + }, +}) diff --git a/web/crux-ui/src/models/image.ts b/web/crux-ui/src/models/image.ts index 6f7285b64f..52854517fb 100644 --- a/web/crux-ui/src/models/image.ts +++ b/web/crux-ui/src/models/image.ts @@ -1,5 +1,5 @@ -import { ContainerConfigData } from './container' -import { BasicRegistry, RegistryImages } from './registry' +import { ContainerConfig, ContainerConfigData, ContainerConfigKey } from './container' +import { BasicRegistry, imageName, RegistryImages } from './registry' export const ENVIRONMENT_VALUE_TYPES = ['string', 'boolean', 'int'] as const export type EnvironmentValueType = (typeof ENVIRONMENT_VALUE_TYPES)[number] @@ -15,7 +15,7 @@ export type VersionImage = { name: string tag: string order: number - config: ContainerConfigData + config: ContainerConfig createdAt: string registry: BasicRegistry labels: Record @@ -24,7 +24,7 @@ export type VersionImage = { export type PatchVersionImage = { tag?: string config?: Partial - resetSection?: ImageConfigProperty + resetSection?: ContainerConfigKey } export type ViewState = 'editor' | 'json' @@ -35,7 +35,6 @@ export type AddImages = { } // ws - export const WS_TYPE_ADD_IMAGES = 'add-images' export type AddImagesMessage = { registryImages: RegistryImages[] @@ -56,9 +55,11 @@ export type ImagesAddedMessage = { images: VersionImage[] } -export const WS_TYPE_PATCH_IMAGE = 'patch-image' -export type PatchImageMessage = PatchVersionImage & { - id: string +export const WS_TYPE_SET_IMAGE_TAG = 'set-image-tag' +export const WS_TYPE_IMAGE_TAG_UPDATED = 'image-tag-updated' +export type ImageTagMessage = { + imageId: string + tag: string } export const WS_TYPE_ORDER_IMAGES = 'order-images' @@ -67,9 +68,6 @@ export type OrderImagesMessage = string[] export const WS_TYPE_IMAGES_WERE_REORDERED = 'images-were-reordered' export type ImagesWereReorderedMessage = string[] -export const WS_TYPE_IMAGE_UPDATED = 'image-updated' -export type ImageUpdateMessage = PatchImageMessage - export const WS_TYPE_GET_IMAGE = 'get-image' export type GetImageMessage = { id: string @@ -78,74 +76,15 @@ export type GetImageMessage = { export const WS_TYPE_IMAGE = 'image' export type ImageMessage = VersionImage -export const COMMON_CONFIG_PROPERTIES = [ - 'name', - 'environment', - 'secrets', - 'routing', - 'expose', - 'user', - 'tty', - 'workingDirectory', - 'configContainer', - 'ports', - 'portRanges', - 'volumes', - 'commands', - 'args', - 'initContainers', - 'storage', -] as const - -export const CRANE_CONFIG_PROPERTIES = [ - 'deploymentStrategy', - 'customHeaders', - 'proxyHeaders', - 'useLoadBalancer', - 'extraLBAnnotations', - 'healthCheckConfig', - 'resourceConfig', - 'labels', - 'annotations', - 'metrics', -] as const - -export const DAGENT_CONFIG_PROPERTIES = [ - 'logConfig', - 'restartPolicy', - 'networkMode', - 'networks', - 'dockerLabels', - 'expectedState', -] as const - -export const ALL_CONFIG_PROPERTIES = [ - ...COMMON_CONFIG_PROPERTIES, - ...CRANE_CONFIG_PROPERTIES, - ...DAGENT_CONFIG_PROPERTIES, -] as const - -export const CRANE_CONFIG_FILTER_VALUES = CRANE_CONFIG_PROPERTIES.filter(it => it !== 'extraLBAnnotations') - -export type CommonConfigProperty = (typeof COMMON_CONFIG_PROPERTIES)[number] -export type CraneConfigProperty = (typeof CRANE_CONFIG_PROPERTIES)[number] -export type DagentConfigProperty = (typeof DAGENT_CONFIG_PROPERTIES)[number] -export type ImageConfigProperty = (typeof ALL_CONFIG_PROPERTIES)[number] - -export type BaseImageConfigFilterType = 'all' | 'common' | 'dagent' | 'crane' - -export const filterContains = ( - filter: CommonConfigProperty | CraneConfigProperty | DagentConfigProperty, - filters: ImageConfigProperty[], -): boolean => filters.includes(filter) - -export const filterEmpty = (filterValues: string[], filters: ImageConfigProperty[]): boolean => - filterValues.filter(x => filters.includes(x as ImageConfigProperty)).length > 0 - -export const imageName = (name: string, tag?: string): string => { - if (!tag) { - return name +export const imageNameOf = (image: VersionImage): string => imageName(image.name, image.tag) + +export const registryImageNameToContainerName = (name: string) => { + if (name.includes('/')) { + return name.split('/').pop() } - return `${name}:${tag}` + return name } + +export const containerNameOfImage = (image: VersionImage) => + image.config.name ?? registryImageNameToContainerName(image.name) diff --git a/web/crux-ui/src/models/index.ts b/web/crux-ui/src/models/index.ts index 11643c799a..afc641e209 100644 --- a/web/crux-ui/src/models/index.ts +++ b/web/crux-ui/src/models/index.ts @@ -1,9 +1,13 @@ export * from './audit' export * from './auth' export * from './common' +export * from './compose' export * from './config-bundle' -export * from './package' export * from './container' +export * from './container-config' +export * from './container-conflict' +export * from './container-merge' +export * from './container-errors' export * from './dashboard' export * from './deployment' export * from './editor' @@ -12,13 +16,13 @@ export * from './image' export * from './instance' export * from './node' export * from './notification' +export * from './package' +export * from './pipeline' export * from './project' export * from './registry' -export * from './pipeline' export * from './storage' export * from './team' export * from './template' export * from './token' export * from './user' export * from './version' -export * from './compose' diff --git a/web/crux-ui/src/models/instance.ts b/web/crux-ui/src/models/instance.ts index 88f334eaab..f340c706ea 100644 --- a/web/crux-ui/src/models/instance.ts +++ b/web/crux-ui/src/models/instance.ts @@ -1,8 +1,11 @@ -import { InstanceContainerConfigData } from './container' -import { VersionImage } from './image' +import { ConcreteContainerConfig } from './container' +import { containerNameOfImage, VersionImage } from './image' export type Instance = { id: string image: VersionImage - config?: InstanceContainerConfigData + config: ConcreteContainerConfig } + +export const containerNameOfInstance = (instance: Instance) => + instance.config.name ?? containerNameOfImage(instance.image) diff --git a/web/crux-ui/src/models/node.ts b/web/crux-ui/src/models/node.ts index 7f0fe9c394..6a13ffba54 100644 --- a/web/crux-ui/src/models/node.ts +++ b/web/crux-ui/src/models/node.ts @@ -1,4 +1,4 @@ -import { PaginationWithDateQuery } from './common' +import { DeploymentStatus, PaginationQuery, PaginationWithDateQuery } from './common' import { Container, ContainerCommand, ContainerIdentifier } from './container' export const NODE_TYPE_VALUES = ['docker', 'k8s'] as const @@ -102,6 +102,11 @@ export type NodeContainerInspection = { inspection: string } +export type NodeDeploymentQuery = PaginationQuery & { + filter?: string + status?: DeploymentStatus +} + // ws export const WS_TYPE_NODE_EVENT = 'event' diff --git a/web/crux-ui/src/models/registry.ts b/web/crux-ui/src/models/registry.ts index 1eff2bd5f6..059e3a7502 100644 --- a/web/crux-ui/src/models/registry.ts +++ b/web/crux-ui/src/models/registry.ts @@ -406,3 +406,11 @@ export const findRegistryByUrl = (registries: Registry[], url: string): Registry !registries || !url ? null : registries.filter(it => it.type !== 'unchecked').find(it => url.startsWith(it.imageUrlPrefix)) + +export const imageName = (name: string, tag?: string): string => { + if (!tag) { + return name + } + + return `${name}:${tag}` +} diff --git a/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx b/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx index c918dd0196..8b0228a7d1 100644 --- a/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx +++ b/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx @@ -1,5 +1,5 @@ -import AddConfigBundleCard from '@app/components/config-bundles/add-config-bundle-card' import ConfigBundleCard from '@app/components/config-bundles/config-bundle-card' +import EditConfigBundleCard from '@app/components/config-bundles/edit-config-bundle-card' import { Layout } from '@app/components/layout' import { BreadcrumbLink } from '@app/components/shared/breadcrumb' import Filters from '@app/components/shared/filters' @@ -11,7 +11,7 @@ import useAnchor from '@app/hooks/use-anchor' import { TextFilter, textFilterFor, useFilters } from '@app/hooks/use-filters' import useSubmit from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { ConfigBundle } from '@app/models' +import { ConfigBundle, ConfigBundleDetails } from '@app/models' import { ANCHOR_NEW, ListRouteOptions, TeamRoutes } from '@app/routes' import { withContextAuthorization } from '@app/utils' import { getCruxFromContext } from '@server/crux-api' @@ -40,7 +40,7 @@ const ConfigBundles = (props: ConfigBundlesPageProps) => { const creating = anchor === ANCHOR_NEW const submit = useSubmit() - const onCreated = async (bundle: ConfigBundle) => { + const onCreated = async (bundle: ConfigBundleDetails) => { await router.push(routes.configBundle.details(bundle.id)) } @@ -59,7 +59,9 @@ const ConfigBundles = (props: ConfigBundlesPageProps) => { - {!creating ? null : } + {!creating ? null : ( + + )} {filters.items.length ? ( <> filters.setFilter({ text: it })} /> diff --git a/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx b/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx index bdec815f33..c07b41f346 100644 --- a/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx @@ -1,26 +1,39 @@ -import { ConfigBundlePageMenu } from '@app/components/config-bundles/config-bundle-page-menu' -import { useConfigBundleDetailsState } from '@app/components/config-bundles/use-config-bundle-details-state' -import MultiInput from '@app/components/editor/multi-input' -import MultiTextArea from '@app/components/editor/multi-textarea' +import ConfigBundleCard from '@app/components/config-bundles/config-bundle-card' +import EditConfigBundleCard from '@app/components/config-bundles/edit-config-bundle-card' +import DeploymentStatusTag from '@app/components/deployments/deployment-status-tag' import { Layout } from '@app/components/layout' import { BreadcrumbLink } from '@app/components/shared/breadcrumb' -import KeyValueInput from '@app/components/shared/key-value-input' import PageHeading from '@app/components/shared/page-heading' +import { DetailsPageMenu } from '@app/components/shared/page-menu' +import { PaginationSettings } from '@app/components/shared/paginator' import { DyoCard } from '@app/elements/dyo-card' -import { DyoHeading } from '@app/elements/dyo-heading' -import { DyoLabel } from '@app/elements/dyo-label' -import WebSocketSaveIndicator from '@app/elements/web-socket-save-indicator' +import DyoIcon from '@app/elements/dyo-icon' +import DyoLink from '@app/elements/dyo-link' +import DyoTable, { DyoColumn, sortDate, sortEnum, sortString } from '@app/elements/dyo-table' import { defaultApiErrorHandler } from '@app/errors' +import usePagination from '@app/hooks/use-pagination' +import useSubmit from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { ConfigBundleDetails } from '@app/models' +import { + ConfigBundleDetails, + Deployment, + DEPLOYMENT_STATUS_VALUES, + DeploymentQuery, + detailsToConfigBundle, + PaginatedList, + PaginationQuery, +} from '@app/models' import { TeamRoutes } from '@app/routes' -import { withContextAuthorization } from '@app/utils' +import { auditToLocaleDate, withContextAuthorization } from '@app/utils' import { getCruxFromContext } from '@server/crux-api' import { GetServerSidePropsContext } from 'next' import useTranslation from 'next-translate/useTranslation' -import toast from 'react-hot-toast' +import { useRouter } from 'next/router' +import { useCallback, useState } from 'react' -interface ConfigBundleDetailsPageProps { +const defaultPagination: PaginationSettings = { pageNumber: 0, pageSize: 10 } + +type ConfigBundleDetailsPageProps = { configBundle: ConfigBundleDetails } @@ -28,24 +41,51 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { const { configBundle: propsConfigBundle } = props const { t } = useTranslation('config-bundles') + const router = useRouter() const routes = useTeamRoutes() - const onWsError = (error: Error) => { - // eslint-disable-next-line - console.error('ws', 'edit-config-bundle', error) - toast(t('errors:connectionLost')) - } - const onApiError = defaultApiErrorHandler(t) - const [state, actions] = useConfigBundleDetailsState({ - configBundle: propsConfigBundle, - onWsError, - onApiError, + const [configBundle, setConfigBundle] = useState(propsConfigBundle) + const [editing, setEditing] = useState(false) + + const submit = useSubmit() + + const fetchData = useCallback( + async (paginationQuery: PaginationQuery): Promise> => { + const query: DeploymentQuery = { + ...paginationQuery, + configBundleId: propsConfigBundle.id, + } + + const res = await fetch(routes.deployment.api.list(query)) + + if (!res.ok) { + await onApiError(res) + return null + } + + return (await res.json()) as PaginatedList + }, + [routes, onApiError], + ) + + const [pagination, setPagination] = usePagination({ + defaultSettings: defaultPagination, + fetchData, }) - const { configBundle, editing, saveState, editorState, fieldErrors, topBarContent } = state - const { setEditing, onDelete, onEditEnv, onEditName, onEditDescription } = actions + const onDelete = async () => { + const res = await fetch(routes.configBundle.api.details(configBundle.id), { + method: 'DELETE', + }) + + if (res.ok) { + await router.replace(routes.configBundle.list()) + } else { + await onApiError(res) + } + } const pageLink: BreadcrumbLink = { name: t('common:configBundles'), @@ -53,7 +93,7 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { } return ( - + { }, ]} > - - - { })} /> - - - - {t(editing ? 'common:editName' : 'view', configBundle)} - - - {t('tips')} - -
- {editing && ( -
-
- {t('common:name')} - - onEditName(it)} - value={configBundle.name} - editorOptions={editorState} - message={fieldErrors.find(it => it.path?.startsWith('name'))?.message} - required - grow - /> -
- -
- - {t('common:description')} - - - onEditDescription(it)} - value={configBundle.description} - editorOptions={editorState} - message={fieldErrors.find(it => it.path?.startsWith('description'))?.message} - required - grow - /> -
-
- )} - - + ) : ( + + )} + + + + + + + it.audit.updatedAt ?? it.audit.createdAt} + sort={sortDate} + body={(it: Deployment) => auditToLocaleDate(it.audit)} + /> + } + /> + ( + + + + )} /> -
+
) diff --git a/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx b/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx new file mode 100644 index 0000000000..ac1fad5f21 --- /dev/null +++ b/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx @@ -0,0 +1,525 @@ +import CommonConfigSection from '@app/components/container-configs/common-config-section' +import configToFilters from '@app/components/container-configs/config-to-filters' +import ContainerConfigFilters from '@app/components/container-configs/container-config-filters' +import ContainerConfigJsonEditor from '@app/components/container-configs/container-config-json-editor' +import CraneConfigSection from '@app/components/container-configs/crane-config-section' +import DagentConfigSection from '@app/components/container-configs/dagent-config-section' +import EditorBadge from '@app/components/editor/editor-badge' +import useEditorState from '@app/components/editor/use-editor-state' +import useItemEditorState from '@app/components/editor/use-item-editor-state' +import { Layout } from '@app/components/layout' +import { BreadcrumbLink } from '@app/components/shared/breadcrumb' +import PageHeading from '@app/components/shared/page-heading' +import { WS_PATCH_DELAY } from '@app/const' +import DyoButton from '@app/elements/dyo-button' +import { DyoCard } from '@app/elements/dyo-card' +import { DyoHeading } from '@app/elements/dyo-heading' +import DyoMessage from '@app/elements/dyo-message' +import WebSocketSaveIndicator from '@app/elements/web-socket-save-indicator' +import useTeamRoutes from '@app/hooks/use-team-routes' +import { CANCEL_THROTTLE, useThrottling } from '@app/hooks/use-throttleing' +import useWebSocket from '@app/hooks/use-websocket' +import { + ConcreteContainerConfigData, + ConfigSecretsMessage, + ConfigUpdatedMessage, + ConflictedContainerConfigData, + conflictsToError, + ContainerConfigData, + ContainerConfigDetails, + ContainerConfigKey, + ContainerConfigRelations, + containerConfigToJsonConfig, + ContainerConfigType, + containerConfigTypeToSectionType, + getConflictsForConcreteConfig, + JsonContainerConfig, + mergeConfigsWithConcreteConfig, + mergeJsonWithContainerConfig, + PatchConfigMessage, + squashConfigs, + ViewState, + WebSocketSaveState, + WS_TYPE_CONFIG_SECRETS, + WS_TYPE_CONFIG_UPDATED, + WS_TYPE_GET_CONFIG_SECRETS, + WS_TYPE_PATCH_CONFIG, + WS_TYPE_PATCH_RECEIVED, +} from '@app/models' +import { TeamRoutes } from '@app/routes' +import { withContextAuthorization } from '@app/utils' +import { + ContainerConfigValidationErrors, + getConcreteContainerConfigFieldErrors, + getContainerConfigFieldErrors, + jsonErrorOf, +} from '@app/validations' +import { WsMessage } from '@app/websockets/common' +import { getCruxFromContext } from '@server/crux-api' +import { GetServerSidePropsContext } from 'next' +import { Translate } from 'next-translate' +import useTranslation from 'next-translate/useTranslation' +import { useCallback, useEffect, useRef, useState } from 'react' + +const pageLinkOf = (t: Translate, url: string, type: ContainerConfigType): BreadcrumbLink => { + switch (type) { + case 'image': + return { + name: t('common:imageConfig'), + url, + } + case 'instance': + return { + name: t('common:instanceConfig'), + url, + } + case 'deployment': + return { + name: t('common:deploymentConfig'), + url, + } + case 'config-bundle': + return { + name: t('common:configBundles'), + url, + } + default: + return { + name: t('common:config'), + url, + } + } +} + +const sublinksOf = ( + routes: TeamRoutes, + type: ContainerConfigType, + relations: ContainerConfigRelations, +): BreadcrumbLink[] => { + const { project, version, deployment, configBundle } = relations + + switch (type) { + case 'image': + return [ + { + name: project.name, + url: routes.project.details(project.id), + }, + { + name: version.name, + url: routes.project.versions(project.id).details(version.id), + }, + ] + case 'instance': + case 'deployment': + return [ + { + name: deployment.prefix, + url: routes.deployment.details(deployment.id), + }, + ] + case 'config-bundle': + return [ + { + name: configBundle.name, + url: routes.configBundle.details(configBundle.id), + }, + ] + default: + return [] + } +} + +const getConfigErrors = ( + config: ContainerConfigDetails, + imageLabels: Record, + t: Translate, +): ContainerConfigValidationErrors => { + const type = containerConfigTypeToSectionType(config.type) + + if (type === 'concrete') { + return getConcreteContainerConfigFieldErrors(config as ConcreteContainerConfigData, imageLabels, t) + } + + return getContainerConfigFieldErrors(config, imageLabels, t) +} + +const getBaseConfig = (config: ContainerConfigDetails, relations: ContainerConfigRelations): ContainerConfigData => { + switch (config.type) { + case 'instance': + return relations.image.config + case 'deployment': + return squashConfigs(relations.deployment.configBundles.map(it => it.config)) + default: + return null + } +} + +const getBundlesNameMap = ( + config: ContainerConfigDetails, + relations: ContainerConfigRelations, +): Record => { + if (config.type !== 'deployment') { + return {} + } + + return Object.fromEntries(relations.deployment.configBundles.map(it => [it.config.id, it.name])) +} + +const getMergedConfig = ( + config: ContainerConfigDetails, + relations: ContainerConfigRelations, +): ContainerConfigDetails => { + const baseConfig = getBaseConfig(config, relations) + if (!baseConfig) { + return config + } + + const concreteConfig = mergeConfigsWithConcreteConfig([baseConfig], config as ConcreteContainerConfigData) + return { + ...config, + ...concreteConfig, + } +} + +const getImageLabels = ( + config: ContainerConfigDetails, + relations: ContainerConfigRelations, +): Record => { + switch (config.type) { + case 'image': + case 'instance': + return relations.image.labels + default: + return {} + } +} + +const getConflicts = ( + config: ContainerConfigDetails, + relations: ContainerConfigRelations, +): ConflictedContainerConfigData => { + if (config.type !== 'deployment') { + return null + } + + const bundles = relations.deployment.configBundles + + if (bundles.length < 2) { + return null + } + + return getConflictsForConcreteConfig( + bundles.map(it => it.config), + config as ConcreteContainerConfigData, + ) +} + +type ContainerConfigPageProps = { + config: ContainerConfigDetails + relations: ContainerConfigRelations +} + +const ContainerConfigPage = (props: ContainerConfigPageProps) => { + const { config: propsConfig, relations } = props + + const { t } = useTranslation('container') + const routes = useTeamRoutes() + + const [resettableConfig, setResettableConfig] = useState(propsConfig) + const [viewState, setViewState] = useState('editor') + const [fieldErrors, setFieldErrors] = useState(() => + getConfigErrors(getMergedConfig(propsConfig, relations), getImageLabels(propsConfig, relations), t), + ) + const [jsonError, setJsonError] = useState(jsonErrorOf(fieldErrors)) + const [topBarContent, setTopBarContent] = useState(null) + const [saveState, setSaveState] = useState(null) + + const [filters, setFilters] = useState(configToFilters([], resettableConfig, fieldErrors)) + const [secrets, setSecrets] = useState(null) + + const patch = useRef>({}) + const throttle = useThrottling(WS_PATCH_DELAY) + const sock = useWebSocket(routes.containerConfig.detailsSocket(resettableConfig.id), { + onOpen: () => { + setSaveState('connected') + sock.send(WS_TYPE_GET_CONFIG_SECRETS, {}) + }, + onClose: () => setSaveState('disconnected'), + onSend: (message: WsMessage) => { + if (message.type === WS_TYPE_PATCH_CONFIG) { + setSaveState('saving') + } + }, + onReceive: (message: WsMessage) => { + if (message.type === WS_TYPE_PATCH_RECEIVED) { + setSaveState('saved') + } + }, + }) + + const editor = useEditorState(sock) + const editorState = useItemEditorState(editor, sock, resettableConfig.id) + + const conflicts = getConflicts(resettableConfig, relations) + const conflictErrors = conflictsToError(t, getBundlesNameMap(resettableConfig, relations), conflicts) + const baseConfig = getBaseConfig(resettableConfig, relations) + const config = getMergedConfig(resettableConfig, relations) + + useEffect(() => { + const reactNode = ( + <> + {editorState.editors.map((it, index) => ( + + ))} + + ) + + setTopBarContent(reactNode) + }, [editorState.editors]) + + const getName = useCallback(() => { + const parentName = config.parent.name + + switch (config.type) { + case 'image': + case 'instance': { + const name = config.name ?? parentName + if (!config.name || config.name === parentName) { + return name + } + + return `${name} (${parentName})` + } + default: + return parentName + } + }, [config.name, config.parent.name, config.type]) + + const getBackHref = useCallback(() => { + switch (propsConfig.type) { + case 'image': + return routes.project.versions(relations.project.id).details(relations.version.id, { section: 'images' }) + case 'instance': + case 'deployment': + return routes.deployment.details(relations.deployment.id) + case 'config-bundle': + return routes.configBundle.details(relations.configBundle.id) + default: + throw new Error(`Unknown ContainerConfigType ${propsConfig.type}`) + } + }, [routes, relations, propsConfig.type]) + + useEffect(() => { + setFilters(current => configToFilters(current, config)) + }, [config]) + + const setErrorsForConfig = useCallback( + newConfig => { + const errors = getConfigErrors(newConfig, getImageLabels(propsConfig, relations), t) + setFieldErrors(errors) + setJsonError(jsonErrorOf(errors)) + }, + [t], + ) + + const onChange = (newConfig: ContainerConfigData) => { + setSaveState('saving') + + const value = { ...resettableConfig, ...newConfig } + setResettableConfig(value) + setErrorsForConfig(value) + + const newPatch = { + ...patch.current, + ...newConfig, + } + patch.current = newPatch + + throttle(() => { + sock.send(WS_TYPE_PATCH_CONFIG, { + id: resettableConfig.id, + config: patch.current, + } as PatchConfigMessage) + + patch.current = {} + }) + } + + const onResetSection = (section: ContainerConfigKey) => { + const newConfig = { ...resettableConfig } as any + newConfig[section] = section === 'user' ? -1 : null + + setResettableConfig(newConfig) + setErrorsForConfig(newConfig) + + throttle(CANCEL_THROTTLE) + sock.send(WS_TYPE_PATCH_CONFIG, { + id: resettableConfig.id, + resetSection: section, + } as PatchConfigMessage) + } + + sock.on(WS_TYPE_CONFIG_UPDATED, (message: ConfigUpdatedMessage) => { + if (message.id !== resettableConfig.id) { + return + } + + const newConfig = { + ...resettableConfig, + ...message, + } + + setResettableConfig(newConfig) + setErrorsForConfig(newConfig) + }) + + sock.on(WS_TYPE_CONFIG_SECRETS, setSecrets) + + const pageLink = pageLinkOf(t, routes.configBundle.details(propsConfig.id), propsConfig.type) + const sublinks = sublinksOf(routes, propsConfig.type, relations) + + const getViewStateButtons = () => ( +
+ setViewState('editor')} + heightClassName="pb-2" + className="mx-8" + > + {t('editor')} + + + setViewState('json')} + className="mx-8" + heightClassName="pb-2" + > + {t('json')} + +
+ ) + + const { mutable } = config.parent + + const sectionType = containerConfigTypeToSectionType(config.type) + const showCraneConfig = sectionType === 'base' || relations.deployment?.node?.type === 'k8s' + const showDagentConfig = sectionType === 'base' || relations.deployment?.node?.type === 'docker' + + return ( + + + + + {t('common:back')} + + + +
+ + {getName()} + + + {getViewStateButtons()} +
+ + {viewState === 'editor' && } +
+ + {viewState === 'editor' && ( + + + + {showCraneConfig && ( + + )} + + {showDagentConfig && ( + + )} + + )} + + {viewState === 'json' && ( + + {jsonError ? ( + + ) : null} + + onChange(mergeJsonWithContainerConfig(config, it))} + onParseError={err => setJsonError(err?.message)} + convertConfigToJson={containerConfigToJsonConfig} + /> + + )} +
+ ) +} + +export default ContainerConfigPage + +const getPageServerSideProps = async (context: GetServerSidePropsContext) => { + const routes = TeamRoutes.fromContext(context) + + const configId = context.query.configId as string + + const config = await getCruxFromContext(context, routes.containerConfig.api.details(configId)) + const relations = await getCruxFromContext( + context, + routes.containerConfig.api.relations(configId), + ) + + return { + props: { + config, + relations, + }, + } +} + +export const getServerSideProps = withContextAuthorization(getPageServerSideProps) diff --git a/web/crux-ui/src/pages/[teamSlug]/dashboard.tsx b/web/crux-ui/src/pages/[teamSlug]/dashboard.tsx index e14d531d46..4052c78b3e 100644 --- a/web/crux-ui/src/pages/[teamSlug]/dashboard.tsx +++ b/web/crux-ui/src/pages/[teamSlug]/dashboard.tsx @@ -113,7 +113,7 @@ const DashboardPage = (props: DashboardPageProps) => { const formatCount = (count: number) => Intl.NumberFormat(lang, { notation: 'compact' }).format(count) const statisticItem = (property: string, count: number) => ( - + +const defaultPagination: PaginationSettings = { pageNumber: 0, pageSize: 10 } -const DeploymentsPage = (props: DeploymentsPageProps) => { - const { deployments: propsDeployments } = props +type FilterState = { + filter: string + status: DeploymentStatus | null + node: DyoNode | null +} +const DeploymentsPage = () => { const { t } = useTranslation('deployments') const routes = useTeamRoutes() const router = useRouter() - const [deployments, setDeployments] = useState(propsDeployments) + const handleApiError = defaultApiErrorHandler(t) + + const [filter, setFilter] = useState({ + filter: '', + status: null, + node: null, + }) const [creating, setCreating] = useState(false) - const handleApiError = defaultApiErrorHandler(t) - const [showInfo, setShowInfo] = useState(null) const [copyDeploymentTarget, setCopyDeploymentTarget] = useCopyDeploymentState({ handleApiError, }) const [confirmModalConfig, confirm] = useConfirmation() - const filters = useFilters({ - filters: [ - textFilterFor(it => [it.project.name, it.version.name, it.node.name, it.prefix]), - enumFilterFor(it => [it.status]), - ], - initialData: deployments, - }) + const throttle = useThrottling(1000) + + const fetchData = useCallback( + async (paginationQuery: PaginationQuery): Promise> => { + const { filter: keywordFilter, node, status } = filter + + const query: DeploymentQuery = { + ...paginationQuery, + filter: !keywordFilter || keywordFilter.trim() === '' ? null : keywordFilter, + nodeId: node?.id ?? null, + status, + } + + const res = await fetch(routes.deployment.api.list(query)) + + if (!res.ok) { + await handleApiError(res) + return null + } + + return (await res.json()) as PaginatedList + }, + [routes, handleApiError, filter], + ) + + const [pagination, setPagination, refreshPage] = usePagination( + { + defaultSettings: defaultPagination, + fetchData, + }, + [filter], + ) - useEffect(() => filters.setItems(deployments), [deployments]) + useEffect(() => { + throttle(refreshPage) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filter]) const selfLink: BreadcrumbLink = { name: t('common:deployments'), @@ -101,7 +137,7 @@ const DeploymentsPage = (props: DeploymentsPageProps) => { return } - setDeployments([...deployments.filter(it => it.id !== deployment.id)]) + refreshPage() } const onDeploymentCopied = async (deploymentId: string) => { @@ -135,121 +171,134 @@ const DeploymentsPage = (props: DeploymentsPageProps) => { /> )} - {deployments.length ? ( - <> - filters.setFilter({ text: it })}> + +
+ + {t('common:filters')} + + +
+ setFilter({ ...filter, filter: e.target.value })} + grow + /> + t(deploymentStatusTranslation(it))} - selection={filters.filter?.enum} + selection={filter.status} onSelectionChange={type => { - filters.setFilter({ - enum: type, + setFilter({ + ...filter, + status: type === 'all' ? null : (type as DeploymentStatus), }) }} qaLabel={chipsQALabelFromValue} /> - - - - - - - - - it.audit.updatedAt ?? it.audit.createdAt} - sort={sortDate} - body={(it: Deployment) => auditToLocaleDate(it.audit)} - /> - } - /> - ( - <> - - - - - 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30', - 'mr-2', - )} - onClick={() => !!it.note && it.note.length > 0 && setShowInfo(it)} - /> - - deploymentIsCopiable(it.status) && setCopyDeploymentTarget(it.id)} - /> - - {deploymentIsDeletable(it.status) ? ( - onDeleteDeployment(it)} - /> - ) : null} - - )} - /> - - - - ) : ( - - {t('noItems')} - - )} +
+
+ +
+ + {t('common:nodes')} + + + setFilter({ ...filter, node: it })} + selection={filter.node} + /> +
+
+ + + + + + + + it.audit.updatedAt ?? it.audit.createdAt} + sort={sortDate} + body={(it: Deployment) => auditToLocaleDate(it.audit)} + /> + } + /> + ( + <> + + + + + 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30', + 'mr-2', + )} + onClick={() => !!it.note && it.note.length > 0 && setShowInfo(it)} + /> + + deploymentIsCopiable(it.status) && setCopyDeploymentTarget(it.id)} + /> + + {deploymentIsDeletable(it.status) ? ( + onDeleteDeployment(it)} + /> + ) : null} + + )} + /> + + {!showInfo ? null : ( { } export default DeploymentsPage - -const getPageServerSideProps = async (context: GetServerSidePropsContext) => { - const routes = TeamRoutes.fromContext(context) - - const deployments = await getCruxFromContext(context, routes.deployment.api.list()) - - return { - props: { - deployments, - }, - } -} - -export const getServerSideProps = withContextAuthorization(getPageServerSideProps) diff --git a/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId].tsx b/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId].tsx index 915320789d..c533364ee2 100644 --- a/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId].tsx @@ -221,7 +221,7 @@ const DeploymentDetailsPage = (props: DeploymentDetailsPageProps) => { )}
- +
diff --git a/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId]/instances/[instanceId].tsx b/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId]/instances/[instanceId].tsx deleted file mode 100644 index a668f4ddf0..0000000000 --- a/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId]/instances/[instanceId].tsx +++ /dev/null @@ -1,312 +0,0 @@ -import EditorBadge from '@app/components/editor/editor-badge' -import useEditorState from '@app/components/editor/use-editor-state' -import useItemEditorState from '@app/components/editor/use-item-editor-state' -import { Layout } from '@app/components/layout' -import useInstanceState from '@app/components/deployments/instances/use-instance-state' -import useDeploymentState from '@app/components/deployments/use-deployment-state' -import CommonConfigSection from '@app/components/projects/versions/images/config/common-config-section' -import configToFilters from '@app/components/projects/versions/images/config/config-to-filters' -import CraneConfigSection from '@app/components/projects/versions/images/config/crane-config-section' -import DagentConfigSection from '@app/components/projects/versions/images/config/dagent-config-section' -import EditImageJson from '@app/components/projects/versions/images/edit-image-json' -import ImageConfigFilters, { - dockerFilterSet, - k8sFilterSet, -} from '@app/components/projects/versions/images/image-config-filters' -import { BreadcrumbLink } from '@app/components/shared/breadcrumb' -import PageHeading from '@app/components/shared/page-heading' -import DyoButton from '@app/elements/dyo-button' -import { DyoCard } from '@app/elements/dyo-card' -import { DyoHeading } from '@app/elements/dyo-heading' -import DyoMessage from '@app/elements/dyo-message' -import WebSocketSaveIndicator from '@app/elements/web-socket-save-indicator' -import { defaultApiErrorHandler } from '@app/errors' -import useTeamRoutes from '@app/hooks/use-team-routes' -import { - DeploymentDetails, - DeploymentRoot, - ImageConfigProperty, - InstanceContainerConfigData, - InstanceJsonContainerConfig, - NodeDetails, - ProjectDetails, - VersionDetails, - ViewState, - instanceConfigToJsonInstanceConfig, - mergeConfigs, - mergeJsonConfigToInstanceContainerConfig, -} from '@app/models' -import { TeamRoutes } from '@app/routes' -import { withContextAuthorization } from '@app/utils' -import { ContainerConfigValidationErrors, getMergedContainerConfigFieldErrors, jsonErrorOf } from '@app/validations' -import { getCruxFromContext } from '@server/crux-api' -import { GetServerSidePropsContext } from 'next' -import useTranslation from 'next-translate/useTranslation' -import { useCallback, useEffect, useState } from 'react' -import toast from 'react-hot-toast' - -interface InstanceDetailsPageProps { - deployment: DeploymentRoot - instanceId: string -} - -const InstanceDetailsPage = (props: InstanceDetailsPageProps) => { - const { deployment, instanceId } = props - const { project, version } = deployment - - const { t } = useTranslation('images') - const routes = useTeamRoutes() - - const onWsError = (error: Error) => { - // eslint-disable-next-line - console.error('ws', 'edit-deployment', error) - toast(t('errors:connectionLost')) - } - - const onApiError = defaultApiErrorHandler(t) - - const [deploymentState, deploymentActions] = useDeploymentState({ - deployment, - onWsError, - onApiError, - }) - - const instance = deploymentState.instances.find(it => it.id === instanceId) - - const [state, actions] = useInstanceState({ - instance, - deploymentState, - deploymentActions, - }) - - const [fieldErrors, setFieldErrors] = useState(() => - getMergedContainerConfigFieldErrors(mergeConfigs(instance.image.config, state.config), instance.image.labels, t), - ) - const [filters, setFilters] = useState(configToFilters([], state.config, fieldErrors)) - const [viewState, setViewState] = useState('editor') - const [jsonError, setJsonError] = useState(jsonErrorOf(fieldErrors)) - const [topBarContent, setTopBarContent] = useState(null) - - const editor = useEditorState(deploymentState.sock) - const editorState = useItemEditorState(editor, deploymentState.sock, instance.id) - - useEffect(() => { - const reactNode = ( - <> - {editorState.editors.map((it, index) => ( - - ))} - - ) - - setTopBarContent(reactNode) - }, [editorState.editors]) - - useEffect(() => { - setFilters(current => configToFilters(current, state.config)) - }, [state.config]) - - const setErrorsForConfig = useCallback( - (imageConfig, instanceConfig) => { - const merged = mergeConfigs(imageConfig, instanceConfig) - const errors = getMergedContainerConfigFieldErrors(merged, instance.image.labels, t) - setFieldErrors(errors) - setJsonError(jsonErrorOf(errors)) - }, - [t], - ) - - useEffect(() => { - setErrorsForConfig(instance.image.config, instance.config) - }, [instance.image.config, instance.config, setErrorsForConfig]) - - const onChange = (newConfig: Partial) => actions.onPatch(instance.id, newConfig) - - const pageLink: BreadcrumbLink = { - name: t('common:container'), - url: routes.project.list(), - } - - const sublinks: BreadcrumbLink[] = [ - { - name: project.name, - url: routes.project.details(project.id), - }, - { - name: version.name, - url: routes.project.versions(project.id).details(version.id), - }, - { - name: t('common:deployment'), - url: routes.deployment.details(deployment.id), - }, - { - name: instance.image.name, - url: routes.deployment.instanceDetails(deployment.id, instance.id), - }, - ] - - const getViewStateButtons = () => ( -
- setViewState('editor')} - heightClassName="pb-2" - className="mx-8" - > - {t('editor')} - - - setViewState('json')} - className="mx-8" - heightClassName="pb-2" - > - {t('json')} - -
- ) - - const kubeNode = deployment.node.type === 'k8s' - - return ( - - - - - {t('common:back')} - - - -
- - {instance.image.name} - {instance.image.name !== state.config?.name ? ` (${state.config?.name})` : null} - - - {getViewStateButtons()} -
- - {viewState === 'editor' && ( - - )} -
- - {viewState === 'editor' && ( - - - - {kubeNode ? ( - - ) : ( - - )} - - )} - - {viewState === 'json' && ( - - {jsonError ? ( - - ) : null} - - - onChange(mergeJsonConfigToInstanceContainerConfig(state.config, it)) - } - onParseError={err => setJsonError(err?.message)} - convertConfigToJson={instanceConfigToJsonInstanceConfig} - /> - - )} -
- ) -} - -export default InstanceDetailsPage - -const getPageServerSideProps = async (context: GetServerSidePropsContext) => { - const routes = TeamRoutes.fromContext(context) - - const deploymentId = context.query.deploymentId as string - const instanceId = context.query.instanceId as string - - const deploymentDetails = await getCruxFromContext( - context, - routes.deployment.api.details(deploymentId), - ) - const project = await getCruxFromContext( - context, - routes.project.api.details(deploymentDetails.project.id), - ) - const node = await getCruxFromContext(context, routes.node.api.details(deploymentDetails.node.id)) - - const version = await getCruxFromContext( - context, - routes.project.versions(deploymentDetails.project.id).api.details(deploymentDetails.version.id), - ) - - const deployment: DeploymentRoot = { - ...deploymentDetails, - project, - version, - node, - } - - return { - props: { - deployment, - instanceId, - }, - } -} - -export const getServerSideProps = withContextAuthorization(getPageServerSideProps) diff --git a/web/crux-ui/src/pages/[teamSlug]/nodes/[nodeId].tsx b/web/crux-ui/src/pages/[teamSlug]/nodes/[nodeId].tsx index a495b3b73f..8e670255ce 100644 --- a/web/crux-ui/src/pages/[teamSlug]/nodes/[nodeId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/nodes/[nodeId].tsx @@ -15,7 +15,7 @@ import { DyoConfirmationModal } from '@app/elements/dyo-modal' import { defaultApiErrorHandler } from '@app/errors' import useSubmit from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { Deployment, NodeDetails } from '@app/models' +import { NodeDetails } from '@app/models' import { TeamRoutes } from '@app/routes' import { withContextAuthorization } from '@app/utils' import { getCruxFromContext } from '@server/crux-api' @@ -26,11 +26,10 @@ import { useSWRConfig } from 'swr' interface NodeDetailsPageProps { node: NodeDetails - deployments: Deployment[] } const NodeDetailsPage = (props: NodeDetailsPageProps) => { - const { node: propsNode, deployments } = props + const { node: propsNode } = props const { t } = useTranslation('nodes') const routes = useTeamRoutes() @@ -123,7 +122,7 @@ const NodeDetailsPage = (props: NodeDetailsPageProps) => { ) : state.section === 'logs' ? ( ) : ( - + )} )} @@ -141,12 +140,10 @@ const getPageServerSideProps = async (context: GetServerSidePropsContext) => { const nodeId = context.query.nodeId as string const node = await getCruxFromContext(context, routes.node.api.details(nodeId)) - const deployments = await getCruxFromContext(context, routes.node.api.deployments(nodeId)) return { props: { node, - deployments, }, } } diff --git a/web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId]/images/[imageId].tsx b/web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId]/images/[imageId].tsx deleted file mode 100644 index 88f11b1c7f..0000000000 --- a/web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId]/images/[imageId].tsx +++ /dev/null @@ -1,347 +0,0 @@ -import EditorBadge from '@app/components/editor/editor-badge' -import useEditorState from '@app/components/editor/use-editor-state' -import useItemEditorState from '@app/components/editor/use-item-editor-state' -import { Layout } from '@app/components/layout' -import CommonConfigSection from '@app/components/projects/versions/images/config/common-config-section' -import configToFilters from '@app/components/projects/versions/images/config/config-to-filters' -import CraneConfigSection from '@app/components/projects/versions/images/config/crane-config-section' -import DagentConfigSection from '@app/components/projects/versions/images/config/dagent-config-section' -import EditImageJson from '@app/components/projects/versions/images/edit-image-json' -import ImageConfigFilters from '@app/components/projects/versions/images/image-config-filters' -import { BreadcrumbLink } from '@app/components/shared/breadcrumb' -import PageHeading from '@app/components/shared/page-heading' -import { IMAGE_WS_REQUEST_DELAY } from '@app/const' -import DyoButton from '@app/elements/dyo-button' -import { DyoCard } from '@app/elements/dyo-card' -import { DyoHeading } from '@app/elements/dyo-heading' -import DyoMessage from '@app/elements/dyo-message' -import { DyoConfirmationModal } from '@app/elements/dyo-modal' -import WebSocketSaveIndicator from '@app/elements/web-socket-save-indicator' -import useConfirmation from '@app/hooks/use-confirmation' -import useTeamRoutes from '@app/hooks/use-team-routes' -import { useThrottling } from '@app/hooks/use-throttleing' -import useWebSocket from '@app/hooks/use-websocket' -import { - ContainerConfigData, - DeleteImageMessage, - ImageConfigProperty, - imageConfigToJsonContainerConfig, - ImageUpdateMessage, - JsonContainerConfig, - mergeJsonConfigToImageContainerConfig, - PatchImageMessage, - ProjectDetails, - VersionDetails, - VersionImage, - ViewState, - WebSocketSaveState, - WS_TYPE_DELETE_IMAGE, - WS_TYPE_IMAGE_UPDATED, - WS_TYPE_PATCH_IMAGE, - WS_TYPE_PATCH_RECEIVED, -} from '@app/models' -import { TeamRoutes } from '@app/routes' -import { withContextAuthorization } from '@app/utils' -import { ContainerConfigValidationErrors, getContainerConfigFieldErrors, jsonErrorOf } from '@app/validations' -import { WsMessage } from '@app/websockets/common' -import { getCruxFromContext } from '@server/crux-api' -import { GetServerSidePropsContext } from 'next' -import useTranslation from 'next-translate/useTranslation' -import { useRouter } from 'next/router' -import { QA_DIALOG_LABEL_DELETE_IMAGE } from 'quality-assurance' -import { useCallback, useEffect, useRef, useState } from 'react' - -interface ImageDetailsPageProps { - project: ProjectDetails - version: VersionDetails - image: VersionImage -} - -const ImageDetailsPage = (props: ImageDetailsPageProps) => { - const { image, project, version } = props - - const { t } = useTranslation('images') - const routes = useTeamRoutes() - - const [config, setConfig] = useState(image.config) - const [viewState, setViewState] = useState('editor') - const [fieldErrors, setFieldErrors] = useState(() => - getContainerConfigFieldErrors(image.config, image.labels, t), - ) - const [jsonError, setJsonError] = useState(jsonErrorOf(fieldErrors)) - const [topBarContent, setTopBarContent] = useState(null) - const [saveState, setSaveState] = useState(null) - - const [filters, setFilters] = useState(configToFilters([], config, fieldErrors)) - - const patch = useRef>({}) - const throttle = useThrottling(IMAGE_WS_REQUEST_DELAY) - const router = useRouter() - const [deleteModalConfig, confirmDelete] = useConfirmation() - const versionSock = useWebSocket(routes.project.versions(project.id).detailsSocket(version.id), { - onOpen: () => setSaveState('connected'), - onClose: () => setSaveState('disconnected'), - onReceive: (message: WsMessage) => { - if (message.type === WS_TYPE_PATCH_RECEIVED) { - setSaveState('saved') - } - }, - }) - - const editor = useEditorState(versionSock) - const editorState = useItemEditorState(editor, versionSock, image.id) - - useEffect(() => { - const reactNode = ( - <> - {editorState.editors.map((it, index) => ( - - ))} - - ) - - setTopBarContent(reactNode) - }, [editorState.editors]) - - useEffect(() => { - setFilters(current => configToFilters(current, config)) - }, [config]) - - const setErrorsForConfig = useCallback( - newConfig => { - const errors = getContainerConfigFieldErrors(newConfig, image.labels, t) - setFieldErrors(errors) - setJsonError(jsonErrorOf(errors)) - }, - [t], - ) - - const onChange = (newConfig: Partial) => { - setSaveState('saving') - - const value = { ...config, ...newConfig } - setConfig(value) - setErrorsForConfig(value) - - const newPatch = { - ...patch.current, - ...newConfig, - } - patch.current = newPatch - - throttle(() => { - versionSock.send(WS_TYPE_PATCH_IMAGE, { - id: image.id, - config: patch.current, - } as PatchImageMessage) - - patch.current = {} - }) - } - - const onResetSection = (section: ImageConfigProperty) => { - const newConfig = { ...config } as any - newConfig[section] = section === 'user' ? -1 : null - - setConfig(newConfig) - setErrorsForConfig(newConfig) - - versionSock.send(WS_TYPE_PATCH_IMAGE, { - id: image.id, - resetSection: section, - } as PatchImageMessage) - } - - versionSock.on(WS_TYPE_IMAGE_UPDATED, (message: ImageUpdateMessage) => { - if (message.id !== image.id) { - return - } - - const newConfig = { - ...config, - ...message.config, - } - - setConfig(newConfig) - setErrorsForConfig(newConfig) - }) - - const onDelete = async () => { - const confirmed = await confirmDelete({ - qaLabel: QA_DIALOG_LABEL_DELETE_IMAGE, - title: t('common:areYouSureDeleteName', { name: image.name }), - description: t('common:proceedYouLoseAllDataToName', { name: image.name }), - confirmText: t('common:delete'), - confirmColor: 'bg-error-red', - }) - - if (!confirmed) { - return - } - - versionSock.send(WS_TYPE_DELETE_IMAGE, { - imageId: image.id, - } as DeleteImageMessage) - - await router.replace(routes.project.versions(project.id).details(version.id)) - } - - const pageLink: BreadcrumbLink = { - name: t('common:image'), - url: routes.project.list(), - } - - const sublinks: BreadcrumbLink[] = [ - { - name: project.name, - url: routes.project.details(project.id), - }, - { - name: version.name, - url: routes.project.versions(project.id).details(version.id), - }, - { - name: image.name, - url: routes.project.versions(project.id).imageDetails(version.id, image.id), - }, - ] - - const getViewStateButtons = () => ( -
- setViewState('editor')} - heightClassName="pb-2" - className="mx-8" - > - {t('editor')} - - - setViewState('json')} - className="mx-8" - heightClassName="pb-2" - > - {t('json')} - -
- ) - - return ( - - - - - - {t('common:back')} - - - - {t('common:delete')} - - - - -
- - {image.name} - {image.name !== config?.name ? ` (${config?.name})` : null} - - - {getViewStateButtons()} -
- - {viewState === 'editor' && } -
- - {viewState === 'editor' && ( - - - - - - - - )} - - {viewState === 'json' && ( - - {jsonError ? ( - - ) : null} - - onChange(mergeJsonConfigToImageContainerConfig(config, it))} - onParseError={err => setJsonError(err?.message)} - convertConfigToJson={imageConfigToJsonContainerConfig} - /> - - )} - - -
- ) -} - -export default ImageDetailsPage - -const getPageServerSideProps = async (context: GetServerSidePropsContext) => { - const routes = TeamRoutes.fromContext(context) - - const projectId = context.query.projectId as string - const versionId = context.query.versionId as string - const imageId = context.query.imageId as string - - const project = getCruxFromContext(context, routes.project.api.details(projectId)) - const version = getCruxFromContext(context, routes.project.versions(projectId).api.details(versionId)) - const image = getCruxFromContext( - context, - routes.project.versions(projectId).api.imageDetails(versionId, imageId), - ) - - return { - props: { - image: await image, - project: await project, - version: await version, - }, - } -} - -export const getServerSideProps = withContextAuthorization(getPageServerSideProps) diff --git a/web/crux-ui/src/routes.ts b/web/crux-ui/src/routes.ts index 0782c73417..902b457bd5 100644 --- a/web/crux-ui/src/routes.ts +++ b/web/crux-ui/src/routes.ts @@ -1,6 +1,14 @@ /* eslint-disable no-underscore-dangle */ import { GetServerSidePropsContext } from 'next' -import { AuditLogQuery, ContainerIdentifier, ContainerOperation, PaginationQuery, VersionSectionsState } from './models' +import { + AuditLogQuery, + ContainerIdentifier, + ContainerOperation, + DeploymentQuery, + NodeDeploymentQuery, + PaginationQuery, + VersionSectionsState, +} from './models' // Routes: export const ROUTE_DOCS = 'https://docs.dyrector.io' @@ -116,6 +124,10 @@ const appendUrlParams = (url: string, params?: AnchorUrlParams): string => { } const urlQuery = (url: string, query: object) => { + if (!query) { + return url + } + const params = Object.entries(query) .map(it => { const [key, value] = it @@ -226,7 +238,7 @@ class NodeApi { audit = (id: string, query: AuditLogQuery) => urlQuery(`${this.details(id)}/audit`, query) - deployments = (id: string) => `${this.details(id)}/deployments` + deployments = (id: string, query?: NodeDeploymentQuery) => urlQuery(`${this.details(id)}/deployments`, query) kick = (id: string) => `${this.details(id)}/kick` @@ -369,8 +381,6 @@ class VersionRoutes { details = (id: string, params?: VersionUrlParams) => appendUrlParams(`${this.root}/${id}`, params) deployments = (versionId: string) => `${this.details(versionId)}/deployments` - - imageDetails = (versionId: string, imageId: string) => `${this.details(versionId)}/images/${imageId}` } // project @@ -436,7 +446,7 @@ class DeploymentApi { this.root = `/api${root}` } - list = () => this.root + list = (query?: DeploymentQuery) => urlQuery(this.root, query) details = (id: string) => `${this.root}/${id}` @@ -480,9 +490,6 @@ class DeploymentRoutes { details = (id: string) => `${this.root}/${id}` deploy = (id: string) => `${this.details(id)}/deploy` - - instanceDetails = (deploymentId: string, instanceId: string) => - `${this.details(deploymentId)}/instances/${instanceId}` } // notification @@ -625,8 +632,42 @@ class PipelineRoutes { socket = () => this.root } -// config bundle +// container config +class ContainerConfigApi { + private readonly root: string + + constructor(root: string) { + this.root = `/api${root}` + } + + details = (id: string) => `${this.root}/${id}` + + relations = (configId: string) => `${this.details(configId)}/relations` +} + +class ContainerConfigRoutes { + private readonly root: string + + constructor(root: string) { + this.root = `${root}/container-configurations` + } + private _api: ContainerConfigApi + + get api() { + if (!this._api) { + this._api = new ContainerConfigApi(this.root) + } + + return this._api + } + + details = (id: string) => `${this.root}/${id}` + + detailsSocket = (id: string) => this.details(id) +} + +// config bundle class ConfigBundleApi { private readonly root: string @@ -637,8 +678,6 @@ class ConfigBundleApi { list = () => this.root details = (id: string) => `${this.root}/${id}` - - options = () => `${this.root}/options` } class ConfigBundleRoutes { @@ -661,8 +700,6 @@ class ConfigBundleRoutes { list = (options?: ListRouteOptions) => appendAnchorWhenDeclared(this.root, ANCHOR_NEW, options?.new) details = (id: string) => `${this.root}/${id}` - - detailsSocket = (id: string) => this.details(id) } export class PackageApi { @@ -734,6 +771,8 @@ export class TeamRoutes { private _pipeline: PipelineRoutes + private _containerConfig: ContainerConfigRoutes + private _configBundle: ConfigBundleRoutes private _package: PackageRoutes @@ -810,6 +849,14 @@ export class TeamRoutes { return this._pipeline } + get containerConfig() { + if (!this._containerConfig) { + this._containerConfig = new ContainerConfigRoutes(this.root) + } + + return this._containerConfig + } + get configBundle() { if (!this._configBundle) { this._configBundle = new ConfigBundleRoutes(this.root) diff --git a/web/crux-ui/src/utils.spec.ts b/web/crux-ui/src/utils.spec.ts index aa71dc33fb..706b0cf463 100644 --- a/web/crux-ui/src/utils.spec.ts +++ b/web/crux-ui/src/utils.spec.ts @@ -3,9 +3,9 @@ import { toNumber } from './utils' describe('toNumber() tests', () => { beforeEach(() => {}) - it('given string return NaN value', () => { + it('given string return null value', () => { const result = toNumber('test-string') - expect(result).toEqual(NaN) + expect(result).toEqual(null) }) it('given negative int return negative value', () => { diff --git a/web/crux-ui/src/utils.ts b/web/crux-ui/src/utils.ts index 90ed125e5f..627a8a74a3 100644 --- a/web/crux-ui/src/utils.ts +++ b/web/crux-ui/src/utils.ts @@ -489,7 +489,7 @@ export const toNumber = (value: string): number => { const parsedValue = Number(value) if (Number.isNaN(parsedValue)) { - return NaN + return null } return parsedValue diff --git a/web/crux-ui/src/validations/common.ts b/web/crux-ui/src/validations/common.ts index 3b043c9317..415715f7a7 100644 --- a/web/crux-ui/src/validations/common.ts +++ b/web/crux-ui/src/validations/common.ts @@ -86,7 +86,7 @@ export const iconRule = yup .label('common:icon') export const nameRule = yup.string().required().trim().min(3).max(70).label('common:name') -export const descriptionRule = yup.string().optional().label('common:description') +export const descriptionRule = yup.string().optional().nullable().label('common:description') export const identityNameRule = yup.string().trim().max(16) export const passwordLengthRule = yup.string().min(8).max(70).label('common:password') export const stringArrayRule = yup.array().of(yup.string()) diff --git a/web/crux-ui/src/validations/config-bundle.ts b/web/crux-ui/src/validations/config-bundle.ts index 0a6a86abea..bf95f5d832 100644 --- a/web/crux-ui/src/validations/config-bundle.ts +++ b/web/crux-ui/src/validations/config-bundle.ts @@ -1,12 +1,8 @@ /* eslint-disable import/prefer-default-export */ import yup from './yup' -import { nameRule } from './common' +import { descriptionRule, nameRule } from './common' -export const configBundleCreateSchema = yup.object().shape({ +export const configBundleSchema = yup.object().shape({ name: nameRule, -}) - -export const configBundlePatchSchema = yup.object().shape({ - name: nameRule.optional().nullable(), - environment: yup.array().optional().nullable().label('config-bundles:environment'), + description: descriptionRule, }) diff --git a/web/crux-ui/src/validations/container.ts b/web/crux-ui/src/validations/container.ts index 50e9cf3b07..5abd0e91c7 100644 --- a/web/crux-ui/src/validations/container.ts +++ b/web/crux-ui/src/validations/container.ts @@ -1,4 +1,4 @@ -import { UID_MAX } from '@app/const' +import { UID_MAX, UID_MIN } from '@app/const' import { CONTAINER_DEPLOYMENT_STRATEGY_VALUES, CONTAINER_EXPOSE_STRATEGY_VALUES, @@ -8,7 +8,7 @@ import { CONTAINER_STATE_VALUES, CONTAINER_VOLUME_TYPE_VALUES, ContainerConfigExposeStrategy, - ContainerConfigPortRange, + ContainerPortRange, ContainerDeploymentStrategyType, ContainerLogDriverType, ContainerNetworkMode, @@ -102,29 +102,33 @@ const portNumberRule = portNumberBaseRule.nullable().required() const exposeRule = yup .mixed() .oneOf([...CONTAINER_EXPOSE_STRATEGY_VALUES]) - .default('none') - .required() + .default(null) + .nullable() + .optional() .label('container:common.expose') const restartPolicyRule = yup .mixed() .oneOf([...CONTAINER_RESTART_POLICY_TYPE_VALUES]) - .default('no') - .required() + .default(null) + .nullable() + .optional() .label('container:dagent.restartPolicy') const networkModeRule = yup .mixed() .oneOf([...CONTAINER_NETWORK_MODE_VALUES]) - .default('bridge') - .required() + .default(null) + .nullable() + .optional() .label('container:dagent.networkMode') const deploymentStrategyRule = yup .mixed() .oneOf([...CONTAINER_DEPLOYMENT_STRATEGY_VALUES]) - .default('recreate') - .required() + .default(null) + .nullable() + .optional() .label('container:crane.deploymentStrategy') const logDriverRule = yup @@ -149,7 +153,7 @@ const configContainerRule = yup path: yup.string().required().label('container:common.path'), keepFiles: yup.boolean().default(false).required().label('container:common.keepFiles'), }) - .default({}) + .default(null) .nullable() .optional() .label('container:common.configContainer') @@ -162,7 +166,7 @@ const healthCheckConfigRule = yup readinessProbe: yup.string().nullable().optional().label('container:crane.readinessProbe'), startupProbe: yup.string().nullable().optional().label('container:crane.startupProbe'), }) - .default({}) + .default(null) .optional() .nullable() .label('container:crane.healthCheckConfig') @@ -188,7 +192,7 @@ const resourceConfigRule = yup .optional(), livenessProbe: yup.string().nullable().label('container:crane.livenessProbe'), }) - .default({}) + .default(null) .nullable() .optional() .label('container:crane.resourceConfig') @@ -209,15 +213,15 @@ const storageRule = yup bucket: storageFieldRule.label('container:common.bucketPath'), path: storageFieldRule.label('container:common.volume'), }) - .default({}) + .default(null) .nullable() .optional() .label('container:common.storage') const createOverlapTest = ( schema: yup.NumberSchema, - portRanges: ContainerConfigPortRange[], - field: Exclude, + portRanges: ContainerPortRange[], + field: Exclude, ) => // eslint-disable-next-line no-template-curly-in-string schema.test('port-range-overlap', 'container:validation.pathOverlapsSomePortranges', value => @@ -255,7 +259,7 @@ const portConfigRule = yup.mixed().when('portRanges', ([portRanges]) => { external: createOverlapTest(portNumberOptionalRule, portRanges, 'external').label('container:common.external'), }), ) - .default([]) + .default(null) .nullable() .optional() .label('container:common.ports') @@ -282,7 +286,7 @@ const portRangeConfigRule = yup .required(), }), ) - .default([]) + .default(null) .nullable() .optional() .label('container:common.portRanges') @@ -301,7 +305,7 @@ const volumeConfigRule = yup type: volumeTypeRule, }), ) - .default([]) + .default(null) .nullable() .optional() .label('container:common.volumes') @@ -325,7 +329,7 @@ const initContainerRule = yup volumes: initContainerVolumeLinkRule.default([]).nullable().label('container:common.volumes'), }), ) - .default([]) + .default(null) .nullable() .optional() .label('container:common.initContainer') @@ -336,7 +340,7 @@ const logConfigRule = yup driver: logDriverRule, options: uniqueKeyValuesSchema.default([]).nullable().label('container:dagent.options'), }) - .default({}) + .default(null) .nullable() .optional() .label('container:dagent.logConfig') @@ -348,7 +352,7 @@ const markerRule = yup service: uniqueKeyValuesSchema.default([]).nullable().label('container:crane.service'), ingress: uniqueKeyValuesSchema.default([]).nullable().label('container:crane.ingress'), }) - .default({}) + .default(null) .nullable() .optional() @@ -373,6 +377,9 @@ const routingRule = yup .optional() .default(null), ) + .default(null) + .nullable() + .optional() .label('container:common.routing') const createMetricsPortRule = (ports: ContainerPort[]) => { @@ -404,9 +411,9 @@ const metricsRule = yup.mixed().when(['ports'], ([ports]) => { port: portRule, }), }) + .default(null) .nullable() .optional() - .default(null) .label('container:crane.metrics') }) @@ -417,7 +424,7 @@ const expectedContainerStateRule = yup timeout: yup.number().default(null).nullable().min(0).label('container:dagent.expectedStateTimeout'), exitCode: yup.number().default(0).nullable().min(-127).max(128).label('container:dagent.expectedExitCode'), }) - .default({}) + .default(null) .nullable() .optional() .label('container:dagent.expectedState') @@ -452,7 +459,7 @@ const validateEnvironmentRule = (rule: EnvironmentRule, index: number, env: Uniq } const testEnvironment = (imageLabels: Record) => (arr: UniqueKeyValue[]) => { - if (!imageLabels) { + if (!imageLabels || !arr) { return true } @@ -494,41 +501,46 @@ const testEnvironment = (imageLabels: Record) => (arr: UniqueKey const createContainerConfigBaseSchema = (imageLabels: Record) => yup.object().shape({ - name: matchContainerName(yup.string().required().label('container:common.containerName')), + name: matchContainerName(yup.string().nullable().optional().label('container:common.containerName')), environment: uniqueKeyValuesSchema - .default([]) + .default(null) .nullable() + .optional() .label('container:common.environment') .test('ruleValidation', 'errors:yup.mixed.required', testEnvironment(imageLabels)), routing: routingRule, expose: exposeRule, - user: yup.number().default(null).min(-1).max(UID_MAX).nullable().label('container:common.user'), + user: yup.number().default(null).min(UID_MIN).max(UID_MAX).nullable().optional().label('container:common.user'), workingDirectory: yup.string().default(null).nullable().optional().label('container:common.workingDirectory'), - tty: yup.boolean().default(false).required().label('container:common.tty'), + tty: yup.boolean().default(null).nullable().optional().label('container:common.tty'), configContainer: configContainerRule, ports: portConfigRule, portRanges: portRangeConfigRule, volumes: volumeConfigRule, - commands: shellCommandSchema.default([]).nullable(), - args: shellCommandSchema.default([]).nullable(), + commands: shellCommandSchema.default(null).nullable().optional(), + args: shellCommandSchema.default(null).nullable().optional(), initContainers: initContainerRule, - capabilities: uniqueKeyValuesSchema.default([]).nullable().label('container:common.capabilities'), + capabilities: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:common.capabilities'), storage: storageRule, // dagent: logConfig: logConfigRule, restartPolicy: restartPolicyRule, networkMode: networkModeRule, - networks: uniqueKeysOnlySchema.default([]).nullable().label('container:dagent.networks'), - dockerLabels: uniqueKeyValuesSchema.default([]).nullable().label('container:dagent.dockerLabels'), + networks: uniqueKeysOnlySchema.default(null).nullable().optional().label('container:dagent.networks'), + dockerLabels: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:dagent.dockerLabels'), expectedState: expectedContainerStateRule, // crane deploymentStrategy: deploymentStrategyRule, - customHeaders: uniqueKeysOnlySchema.default([]).nullable().label('container:crane.customHeaders'), - proxyHeaders: yup.boolean().default(false).required().label('container:crane.proxyHeaders'), - useLoadBalancer: yup.boolean().default(false).required().label('container:crane.useLoadBalancer'), - extraLBAnnotations: uniqueKeyValuesSchema.default([]).nullable().label('container:crane.extraLBAnnotations'), + customHeaders: uniqueKeysOnlySchema.default(null).nullable().optional().label('container:crane.customHeaders'), + proxyHeaders: yup.boolean().default(null).nullable().optional().label('container:crane.proxyHeaders'), + useLoadBalancer: yup.boolean().default(null).nullable().optional().label('container:crane.useLoadBalancer'), + extraLBAnnotations: uniqueKeyValuesSchema + .default(null) + .nullable() + .optional() + .label('container:crane.extraLBAnnotations'), healthCheckConfig: healthCheckConfigRule, resourceConfig: resourceConfigRule, labels: markerRule.label('container:crane.labels'), @@ -538,10 +550,10 @@ const createContainerConfigBaseSchema = (imageLabels: Record) => export const createContainerConfigSchema = (imageLabels: Record) => createContainerConfigBaseSchema(imageLabels).shape({ - secrets: uniqueKeySchema.default([]).nullable().label('container:common.secrets'), + secrets: uniqueKeySchema.default(null).nullable().optional().label('container:common.secrets'), }) -export const createMergedContainerConfigSchema = (imageLabels: Record) => +export const createConcreteContainerConfigSchema = (imageLabels: Record) => createContainerConfigBaseSchema(imageLabels).shape({ - secrets: uniqueKeyValuesSchema.default([]).nullable().label('container:common.secrets'), + secrets: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:common.secrets'), }) diff --git a/web/crux-ui/src/validations/deployment.ts b/web/crux-ui/src/validations/deployment.ts index 0f03a89d61..88f9eeca76 100644 --- a/web/crux-ui/src/validations/deployment.ts +++ b/web/crux-ui/src/validations/deployment.ts @@ -1,6 +1,6 @@ import yup from './yup' import { nameRule } from './common' -import { createMergedContainerConfigSchema, uniqueKeyValuesSchema } from './container' +import { createConcreteContainerConfigSchema, uniqueKeyValuesSchema } from './container' export const prefixRule = yup .string() @@ -10,8 +10,9 @@ export const prefixRule = yup .label('common:prefix') export const updateDeploymentSchema = yup.object().shape({ - note: yup.string().label('common:note'), + note: yup.string().optional().nullable().label('common:note'), prefix: prefixRule, + protected: yup.bool().required(), }) export const createDeploymentSchema = updateDeploymentSchema.concat( @@ -40,7 +41,7 @@ export const startDeploymentSchema = yup.object({ instances: yup .array( yup.object().shape({ - config: createMergedContainerConfigSchema(null), + config: createConcreteContainerConfigSchema(null), }), ) .ensure() diff --git a/web/crux-ui/src/validations/instance.ts b/web/crux-ui/src/validations/instance.ts index aaec2ecd66..e4f038f645 100644 --- a/web/crux-ui/src/validations/instance.ts +++ b/web/crux-ui/src/validations/instance.ts @@ -1,12 +1,12 @@ -import { MergedContainerConfigData } from '@app/models' +import { ConcreteContainerConfigData } from '@app/models' import { Translate } from 'next-translate' import { getConfigFieldErrorsForSchema, ContainerConfigValidationErrors } from './image' -import { createMergedContainerConfigSchema } from './container' +import { createConcreteContainerConfigSchema } from './container' // eslint-disable-next-line import/prefer-default-export -export const getMergedContainerConfigFieldErrors = ( - newConfig: MergedContainerConfigData, +export const getConcreteContainerConfigFieldErrors = ( + newConfig: ConcreteContainerConfigData, validation: Record, t: Translate, ): ContainerConfigValidationErrors => - getConfigFieldErrorsForSchema(createMergedContainerConfigSchema(validation), newConfig, t) + getConfigFieldErrorsForSchema(createConcreteContainerConfigSchema(validation), newConfig, t) diff --git a/web/crux-ui/src/websockets/common.ts b/web/crux-ui/src/websockets/common.ts index 81a8725944..15c48f75be 100644 --- a/web/crux-ui/src/websockets/common.ts +++ b/web/crux-ui/src/websockets/common.ts @@ -25,7 +25,7 @@ export type WsMessageCallback = (message: T) => void export type WsErrorHandler = (message: WsErrorMessage) => void -export type WebSocketSendMessage = (message: WsMessage) => boolean +export type WebSocketClientSendMessage = (message: WsMessage) => boolean export type WebSocketClientOptions = { onOpen?: VoidFunction diff --git a/web/crux-ui/src/websockets/websocket-client-endpoint.ts b/web/crux-ui/src/websockets/websocket-client-endpoint.ts index f2fd5ea8dc..b0e3f652ab 100644 --- a/web/crux-ui/src/websockets/websocket-client-endpoint.ts +++ b/web/crux-ui/src/websockets/websocket-client-endpoint.ts @@ -1,7 +1,7 @@ -import { WebSocketClientOptions, WebSocketSendMessage, WsMessage, WsMessageCallback } from './common' +import { WebSocketClientOptions, WebSocketClientSendMessage, WsMessage, WsMessageCallback } from './common' class WebSocketClientEndpoint { - private sendClientMessage: WebSocketSendMessage = null + private sendClientMessage: WebSocketClientSendMessage = null private callbacks: Map> = new Map() @@ -78,7 +78,7 @@ class WebSocketClientEndpoint { callbacks?.forEach(it => it(msg.data)) } - onSubscribed(sendClientMessage: WebSocketSendMessage): void { + onSubscribed(sendClientMessage: WebSocketClientSendMessage): void { this.sendClientMessage = sendClientMessage if (this.readyStateChanged) { diff --git a/web/crux-ui/src/websockets/websocket-client-route.ts b/web/crux-ui/src/websockets/websocket-client-route.ts index 01b755f7a6..b95812ed95 100644 --- a/web/crux-ui/src/websockets/websocket-client-route.ts +++ b/web/crux-ui/src/websockets/websocket-client-route.ts @@ -1,5 +1,5 @@ import { Logger } from '@app/logger' -import { SubscriptionMessage, SubscriptionMessageType, WebSocketSendMessage, WsMessage } from './common' +import { SubscriptionMessage, SubscriptionMessageType, WebSocketClientSendMessage, WsMessage } from './common' import WebSocketClientEndpoint from './websocket-client-endpoint' type WebSocketClientRouteState = 'subscribed' | 'unsubscribed' | 'in-progress' @@ -7,7 +7,7 @@ type WebSocketClientRouteState = 'subscribed' | 'unsubscribed' | 'in-progress' class WebSocketClientRoute { private logger: Logger - private readonly sendClientMessage: WebSocketSendMessage + private readonly sendClientMessage: WebSocketClientSendMessage private state: WebSocketClientRouteState = 'unsubscribed' @@ -19,7 +19,7 @@ class WebSocketClientRoute { constructor( logger: Logger, - private readonly sendMessage: WebSocketSendMessage, + private readonly sendMessage: WebSocketClientSendMessage, private endpointPath: string, ) { this.logger = logger.derive(endpointPath) diff --git a/web/crux/package.json b/web/crux/package.json index 4c92c568b9..936f801814 100644 --- a/web/crux/package.json +++ b/web/crux/package.json @@ -1,6 +1,6 @@ { "name": "crux", - "version": "0.14.1", + "version": "0.15.0-rc", "description": "Open-source delivery platform that helps developers to deliver applications efficiently by simplifying software releases and operations in any environment.", "author": "dyrector.io", "private": true, diff --git a/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql b/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql new file mode 100644 index 0000000000..233300debb --- /dev/null +++ b/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql @@ -0,0 +1,250 @@ +-- CreateEnum +CREATE TYPE "ContainerConfigType" AS ENUM ('image', 'instance', 'deployment', 'configBundle'); + +-- DropForeignKey +ALTER TABLE "ContainerConfig" DROP CONSTRAINT "ContainerConfig_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "InstanceContainerConfig" DROP CONSTRAINT "InstanceContainerConfig_instanceId_fkey"; + +-- DropForeignKey +ALTER TABLE "InstanceContainerConfig" DROP CONSTRAINT "InstanceContainerConfig_storageId_fkey"; + +-- DropIndex +DROP INDEX "ContainerConfig_imageId_key"; + +-- ContainerConfig and Image +-- reverse ContainerConfig -> Image relation +ALTER TABLE "Image" ADD COLUMN "configId" UUID; + +UPDATE "Image" +SET "configId" = "cc"."id" +FROM (SELECT DISTINCT "id", "imageId" FROM "ContainerConfig") AS "cc" +WHERE "cc"."imageId" = "Image"."id"; + +-- add ContainerConfigType +ALTER TABLE "ContainerConfig" +ADD COLUMN "type" "ContainerConfigType"; + +UPDATE "ContainerConfig" +SET "type" = 'image'::"ContainerConfigType"; + +ALTER TABLE "ContainerConfig" +ALTER COLUMN "type" SET NOT NULL; + +-- add audit fields +ALTER TABLE "ContainerConfig" +ADD COLUMN "updatedAt" TIMESTAMPTZ(6), +ADD COLUMN "updatedBy" TEXT; + +UPDATE "ContainerConfig" +SET "updatedAt" = "i"."updatedAt", + "updatedBy" = "i"."updatedBy" +FROM (SELECT "id", "updatedAt", "updatedBy" FROM "Image") AS "i" +WHERE "i"."id" = "ContainerConfig"."imageId"; + +ALTER TABLE "ContainerConfig" +ALTER COLUMN "updatedAt" SET NOT NULL; + +-- drop imageId +ALTER TABLE "ContainerConfig" DROP COLUMN "imageId"; + +-- drop not nulls and defaults +ALTER TABLE "ContainerConfig" +ALTER COLUMN "name" DROP NOT NULL, +ALTER COLUMN "expose" DROP NOT NULL, +ALTER COLUMN "user" DROP NOT NULL, +ALTER COLUMN "user" DROP DEFAULT, +ALTER COLUMN "restartPolicy" DROP NOT NULL, +ALTER COLUMN "networkMode" DROP NOT NULL, +ALTER COLUMN "deploymentStrategy" DROP NOT NULL, +ALTER COLUMN "proxyHeaders" DROP NOT NULL, +ALTER COLUMN "tty" DROP NOT NULL, +ALTER COLUMN "useLoadBalancer" DROP NOT NULL; + +-- Image + +-- add missing configs +SELECT "i"."id" +INTO "_prisma_migrations_ConfiglessImages" +FROM "Image" AS "i" +WHERE "i"."configId" IS NULL; + +UPDATE "Image" +SET "configId" = gen_random_uuid() +WHERE "Image"."id" IN (SELECT id FROM "_prisma_migrations_ConfiglessImages"); + +INSERT INTO "ContainerConfig" +("id", "type", "updatedAt", "updatedBy") +SELECT "i"."configId", 'image'::"ContainerConfigType", "i"."updatedAt", "i"."updatedBy" +FROM "Image" AS "i" +WHERE "i"."id" IN (SELECT id FROM "_prisma_migrations_ConfiglessImages"); + +DROP TABLE "_prisma_migrations_ConfiglessImages"; + + +-- ConfigBundle +ALTER TABLE "ConfigBundle" +ADD COLUMN "configId" UUID; + +UPDATE "ConfigBundle" +SET "configId" = gen_random_uuid(); + +INSERT INTO "ContainerConfig" +("id", "type", "updatedAt", "updatedBy", "environment") +SELECT "configId", 'configBundle'::"ContainerConfigType", "updatedAt", "updatedBy", "data" +FROM "ConfigBundle"; + +ALTER TABLE "ConfigBundle" +DROP COLUMN "data"; + + +-- Deployment +ALTER TABLE "Deployment" +ADD COLUMN "configId" UUID; + +UPDATE "Deployment" +SET "configId" = gen_random_uuid(); + +INSERT INTO "ContainerConfig" +("id", "type", "updatedAt", "updatedBy", "environment") +SELECT "configId", 'deployment'::"ContainerConfigType", "updatedAt", "updatedBy", "environment" +FROM "Deployment"; + + +ALTER TABLE "Deployment" +DROP COLUMN "environment"; + + +-- Instance +ALTER TABLE "Instance" +ADD COLUMN "configId" UUID; + +UPDATE "Instance" +SET "configId" = "i"."id" +FROM (SELECT DISTINCT "id", "instanceId" FROM "InstanceContainerConfig") AS "i" +WHERE "i"."instanceId" = "Instance"."id"; + + +-- fix instance config +UPDATE "InstanceContainerConfig" SET "name" = null WHERE "name" = 'null'; +UPDATE "InstanceContainerConfig" SET "environment" = null WHERE "environment" = 'null'; +UPDATE "InstanceContainerConfig" SET "secrets" = null WHERE "secrets" = 'null'; +UPDATE "InstanceContainerConfig" SET "capabilities" = null WHERE "capabilities" = 'null'; +UPDATE "InstanceContainerConfig" SET "configContainer" = null WHERE "configContainer" = 'null'; +UPDATE "InstanceContainerConfig" SET "ports" = null WHERE "ports" = 'null'; +UPDATE "InstanceContainerConfig" SET "portRanges" = null WHERE "portRanges" = 'null'; +UPDATE "InstanceContainerConfig" SET "volumes" = null WHERE "volumes" = 'null'; +UPDATE "InstanceContainerConfig" SET "commands" = null WHERE "commands" = 'null'; +UPDATE "InstanceContainerConfig" SET "args" = null WHERE "args" = 'null'; +UPDATE "InstanceContainerConfig" SET "initContainers" = null WHERE "initContainers" = 'null'; +UPDATE "InstanceContainerConfig" SET "logConfig" = null WHERE "logConfig" = 'null'; +UPDATE "InstanceContainerConfig" SET "networks" = null WHERE "networks" = 'null'; +UPDATE "InstanceContainerConfig" SET "dockerLabels" = null WHERE "dockerLabels" = 'null'; +UPDATE "InstanceContainerConfig" SET "healthCheckConfig" = null WHERE "healthCheckConfig" = 'null'; +UPDATE "InstanceContainerConfig" SET "resourceConfig" = null WHERE "resourceConfig" = 'null'; +UPDATE "InstanceContainerConfig" SET "extraLBAnnotations" = null WHERE "extraLBAnnotations" = 'null'; +UPDATE "InstanceContainerConfig" SET "customHeaders" = null WHERE "customHeaders" = 'null'; +UPDATE "InstanceContainerConfig" SET "annotations" = null WHERE "annotations" = 'null'; +UPDATE "InstanceContainerConfig" SET "labels" = null WHERE "labels" = 'null'; +UPDATE "InstanceContainerConfig" SET "storageConfig" = null WHERE "storageConfig" = 'null'; +UPDATE "InstanceContainerConfig" SET "routing" = null WHERE "routing" = 'null'; +UPDATE "InstanceContainerConfig" SET "metrics" = null WHERE "metrics" = 'null'; +UPDATE "InstanceContainerConfig" SET "workingDirectory" = null WHERE "workingDirectory" = 'null'; +UPDATE "InstanceContainerConfig" SET "expectedState" = null WHERE "expectedState" = 'null'; + + +INSERT INTO "ContainerConfig" ( + "id", "updatedAt", "updatedBy", "type", + -- common + "name", "environment", "secrets", "capabilities", "expose", "routing", "configContainer", "user", + "workingDirectory", "tty", "ports", "portRanges", "volumes", "commands", "args", "initContainers", + "storageId", "storageSet", "storageConfig", "expectedState", + -- dagent + "logConfig", "restartPolicy", "networkMode", "networks", "dockerLabels", + -- crane + "deploymentStrategy", "healthCheckConfig", "resourceConfig", "proxyHeaders", "useLoadBalancer", + "extraLBAnnotations", "customHeaders", "annotations", "labels", "metrics" +) +SELECT + "InstanceContainerConfig"."id", "i"."updatedAt", "d"."updatedBy", 'instance'::"ContainerConfigType", + -- common + "name", "environment", "secrets", "capabilities", "expose", "routing", "configContainer", "user", + "workingDirectory", "tty", "ports", "portRanges", "volumes", "commands", "args", "initContainers", + "storageId", "storageSet", "storageConfig", "expectedState", + -- dagent + "logConfig", "restartPolicy", "networkMode", "networks", "dockerLabels", + -- crane + "deploymentStrategy", "healthCheckConfig", "resourceConfig", "proxyHeaders", "useLoadBalancer", + "extraLBAnnotations", "customHeaders", "annotations", "labels", "metrics" +FROM "InstanceContainerConfig" +INNER JOIN "Instance" AS "i" ON "i"."id" = "InstanceContainerConfig"."instanceId" +INNER JOIN "Deployment" AS "d" ON "d"."id" = "i"."deploymentId"; + +ALTER TABLE "Instance" +DROP COLUMN "updatedAt"; + +DROP TABLE "InstanceContainerConfig"; + +-- add missing configs +SELECT "i"."id" +INTO "_prisma_migrations_ConfiglessInstances" +FROM "Instance" AS "i" +WHERE "i"."configId" IS NULL; + +UPDATE "Instance" +SET "configId" = gen_random_uuid() +WHERE "Instance"."id" IN (SELECT id FROM "_prisma_migrations_ConfiglessInstances"); + +INSERT INTO "ContainerConfig" +("id", "type", "updatedAt", "updatedBy") +SELECT "i"."configId", 'instance'::"ContainerConfigType", "d"."updatedAt", "d"."updatedBy" +FROM "Instance" AS "i" +INNER JOIN "Deployment" AS "d" ON "d".id = "i"."deploymentId" +WHERE "i"."id" IN (SELECT id FROM "_prisma_migrations_ConfiglessInstances"); + +DROP TABLE "_prisma_migrations_ConfiglessInstances"; + +-- add deployedAt and deployedBy +ALTER TABLE "Deployment" ADD COLUMN "deployedAt" TIMESTAMPTZ(6), +ADD COLUMN "deployedBy" UUID; + +UPDATE "Deployment" +SET "deployedAt" = "d"."updatedAt" +FROM (select "id", "updatedAt" FROM "Deployment") AS "d" +WHERE "d"."id" = "Deployment"."id"; + +-- set config id not null +ALTER TABLE "ConfigBundle" ALTER COLUMN "configId" SET NOT NULL; + +ALTER TABLE "Deployment" ALTER COLUMN "configId" SET NOT NULL; + +ALTER TABLE "Image" ALTER COLUMN "configId" SET NOT NULL; + +ALTER TABLE "Instance" ALTER COLUMN "configId" SET NOT NULL; + + +-- create indices and constraints +-- CreateIndex +CREATE UNIQUE INDEX "ConfigBundle_configId_key" ON "ConfigBundle"("configId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Deployment_configId_key" ON "Deployment"("configId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Image_configId_key" ON "Image"("configId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Instance_configId_key" ON "Instance"("configId"); + +-- AddForeignKey +ALTER TABLE "Image" ADD CONSTRAINT "Image_configId_fkey" FOREIGN KEY ("configId") REFERENCES "ContainerConfig"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Deployment" ADD CONSTRAINT "Deployment_configId_fkey" FOREIGN KEY ("configId") REFERENCES "ContainerConfig"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Instance" ADD CONSTRAINT "Instance_configId_fkey" FOREIGN KEY ("configId") REFERENCES "ContainerConfig"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ConfigBundle" ADD CONSTRAINT "ConfigBundle_configId_fkey" FOREIGN KEY ("configId") REFERENCES "ContainerConfig"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/web/crux/prisma/schema.prisma b/web/crux/prisma/schema.prisma index a093091c2f..57ac00b025 100644 --- a/web/crux/prisma/schema.prisma +++ b/web/crux/prisma/schema.prisma @@ -203,22 +203,27 @@ model VersionsOnParentVersion { } model Image { - id String @id @default(uuid()) @db.Uuid - name String - tag String? - order Int - versionId String @db.Uuid - registryId String @db.Uuid - config ContainerConfig? - instances Instance[] - createdAt DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - updatedAt DateTime @updatedAt @db.Timestamptz(6) - updatedBy String? @db.Uuid - labels Json? + id String @id @default(uuid()) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + updatedAt DateTime @updatedAt @db.Timestamptz(6) + updatedBy String? @db.Uuid - registry Registry @relation(fields: [registryId], references: [id], onDelete: Cascade) - version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) + name String + tag String? + order Int + labels Json? + + registry Registry @relation(fields: [registryId], references: [id], onDelete: Cascade) + registryId String @db.Uuid + + version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) + versionId String @db.Uuid + + config ContainerConfig @relation(fields: [configId], references: [id], onDelete: Cascade) + configId String @unique @db.Uuid + + instances Instance[] } enum NetworkMode { @@ -245,20 +250,31 @@ enum ExposeStrategy { exposeWithTls } +enum ContainerConfigType { + image + instance + deployment + configBundle +} + model ContainerConfig { - id String @id @default(uuid()) @db.Uuid + id String @id @default(uuid()) @db.Uuid + updatedAt DateTime @updatedAt @db.Timestamptz(6) + updatedBy String? + + type ContainerConfigType //Common - name String + name String? environment Json? secrets Json? capabilities Json? - expose ExposeStrategy + expose ExposeStrategy? routing Json? configContainer Json? - user Int @default(-1) + user Int? workingDirectory String? - tty Boolean + tty Boolean? ports Json? portRanges Json? volumes Json? @@ -266,62 +282,70 @@ model ContainerConfig { args Json? initContainers Json? storageSet Boolean? - storageId String? @db.Uuid storageConfig Json? expectedState Json? //Dagent logConfig Json? - restartPolicy RestartPolicy - networkMode NetworkMode + restartPolicy RestartPolicy? + networkMode NetworkMode? networks Json? dockerLabels Json? //Crane - deploymentStrategy DeploymentStrategy + deploymentStrategy DeploymentStrategy? healthCheckConfig Json? resourceConfig Json? - proxyHeaders Boolean - useLoadBalancer Boolean + proxyHeaders Boolean? + useLoadBalancer Boolean? extraLBAnnotations Json? customHeaders Json? annotations Json? labels Json? metrics Json? - image Image @relation(fields: [imageId], references: [id], onDelete: Cascade) - imageId String @unique @db.Uuid + image Image? + instance Instance? + deployment Deployment? + configBundle ConfigBundle? - storage Storage? @relation(fields: [storageId], references: [id], onDelete: Cascade) + storage Storage? @relation(fields: [storageId], references: [id], onDelete: Cascade) + storageId String? @db.Uuid } model Deployment { - id String @id @default(uuid()) @db.Uuid - createdAt DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - updatedAt DateTime @updatedAt @db.Timestamptz(6) - updatedBy String? @db.Uuid - note String? - prefix String? - status DeploymentStatusEnum - environment Json? - versionId String @db.Uuid - nodeId String @db.Uuid - tries Int @default(0) - protected Boolean @default(false) - - version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) - node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade) + id String @id @default(uuid()) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + updatedAt DateTime @updatedAt @db.Timestamptz(6) + updatedBy String? @db.Uuid + deployedAt DateTime? @db.Timestamptz(6) + deployedBy String? @db.Uuid + + note String? + prefix String? + status DeploymentStatusEnum + tries Int @default(0) + protected Boolean @default(false) + + version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) + versionId String @db.Uuid + + node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade) + nodeId String @db.Uuid + + config ContainerConfig @relation(fields: [configId], references: [id], onDelete: Cascade) + configId String @unique @db.Uuid instances Instance[] events DeploymentEvent[] - tokens DeploymentToken[] + token DeploymentToken? configBundles ConfigBundleOnDeployments[] } model DeploymentToken { id String @id @default(uuid()) @db.Uuid - deploymentId String @db.Uuid + deploymentId String @unique @db.Uuid createdBy String @db.Uuid createdAt DateTime @default(now()) @db.Timestamptz(6) name String @@ -331,68 +355,20 @@ model DeploymentToken { deployment Deployment @relation(fields: [deploymentId], references: [id], onDelete: Cascade) AuditLog AuditLog[] - @@unique([deploymentId]) @@unique([deploymentId, nonce]) } model Instance { - id String @id @default(uuid()) @db.Uuid - updatedAt DateTime @updatedAt @db.Timestamptz(6) - deploymentId String @db.Uuid - imageId String @db.Uuid - - deployment Deployment @relation(fields: [deploymentId], references: [id], onDelete: Cascade) - image Image @relation(fields: [imageId], references: [id], onDelete: Cascade) - config InstanceContainerConfig? -} - -model InstanceContainerConfig { - id String @id @default(uuid()) @db.Uuid - instanceId String @unique @db.Uuid - - //Common - name String? - environment Json? - secrets Json? - capabilities Json? - expose ExposeStrategy? - routing Json? - configContainer Json? - user Int? - workingDirectory String? - tty Boolean? - ports Json? - portRanges Json? - volumes Json? - commands Json? - args Json? - initContainers Json? - storageSet Boolean? - storageId String? @unique @db.Uuid - storageConfig Json? - expectedState Json? + id String @id @default(uuid()) @db.Uuid - //Dagent - logConfig Json? - restartPolicy RestartPolicy? - networkMode NetworkMode? - networks Json? - dockerLabels Json? + deployment Deployment @relation(fields: [deploymentId], references: [id], onDelete: Cascade) + deploymentId String @db.Uuid - //Crane - deploymentStrategy DeploymentStrategy? - healthCheckConfig Json? - resourceConfig Json? - proxyHeaders Boolean? - useLoadBalancer Boolean? - extraLBAnnotations Json? - customHeaders Json? - annotations Json? - labels Json? - metrics Json? + image Image @relation(fields: [imageId], references: [id], onDelete: Cascade) + imageId String @db.Uuid - instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - storage Storage? @relation(fields: [storageId], references: [id], onDelete: Cascade) + config ContainerConfig @relation(fields: [configId], references: [id], onDelete: Cascade) + configId String @unique @db.Uuid } model DeploymentEvent { @@ -579,7 +555,6 @@ model Storage { teamId String @db.Uuid containerConfigs ContainerConfig[] - instanceConfigs InstanceContainerConfig[] @@unique([name, teamId]) } @@ -665,18 +640,21 @@ model PipelineEventWatcher { } model ConfigBundle { - id String @id @default(uuid()) @db.Uuid + id String @id @default(uuid()) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + updatedAt DateTime @updatedAt @db.Timestamptz(6) + updatedBy String? @db.Uuid + name String description String? - data Json - createdAt DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - updatedAt DateTime @updatedAt @db.Timestamptz(6) - updatedBy String? @db.Uuid team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) teamId String @db.Uuid + config ContainerConfig? @relation(fields: [configId], references: [id], onDelete: Cascade) + configId String @unique @db.Uuid + deployments ConfigBundleOnDeployments[] @@unique([name, teamId]) diff --git a/web/crux/proto/agent.proto b/web/crux/proto/agent.proto index 90ae346d5b..57d949f538 100644 --- a/web/crux/proto/agent.proto +++ b/web/crux/proto/agent.proto @@ -36,7 +36,6 @@ service Agent { rpc ContainerLogStream(stream common.ContainerLogMessage) returns (common.Empty); - // one-shot requests rpc SecretList(common.ListSecretsResponse) returns (common.Empty); rpc DeleteContainers(common.Empty) returns (common.Empty); @@ -57,7 +56,7 @@ message AgentInfo { message AgentCommand { oneof command { - VersionDeployRequest deploy = 1; + DeployRequest deploy = 1; ContainerStateRequest containerState = 2; ContainerDeleteRequest containerDelete = 3; DeployRequestLegacy deployLegacy = 4; @@ -92,31 +91,20 @@ message AgentCommandError { */ message DeployResponse { bool started = 1; } -message VersionDeployRequest { +message DeployRequest { string id = 1; string versionName = 2; string releaseNotes = 3; + string prefix = 4; - repeated DeployRequest requests = 4; + map secrets = 5; + repeated DeployWorkloadRequest requests = 6; } /* * Request for a keys of existing secrets in a prefix, eg. namespace */ -message ListSecretsRequest { common.ContainerIdentifier container = 1; } - -/** - * Deploys a single container - * - */ -message InstanceConfig { - /* - prefix mapped into host folder structure, - used as namespace id - */ - string prefix = 1; - optional string mountPath = 2; // mount path of instance (docker only) - map environment = 3; // environment variable map - optional string repositoryPrefix = 4; // registry repo prefix +message ListSecretsRequest { + common.ContainerOrPrefix target = 1; } message RegistryAuth { @@ -241,25 +229,19 @@ message CommonContainerConfig { repeated InitContainer initContainers = 1007; } -message DeployRequest { +message DeployWorkloadRequest { string id = 1; - string containerName = 2; - - /* InstanceConfig is set for multiple containers */ - InstanceConfig instanceConfig = 3; /* ContainerConfigs */ - optional CommonContainerConfig common = 4; - optional DagentContainerConfig dagent = 5; - optional CraneContainerConfig crane = 6; + optional CommonContainerConfig common = 2; + optional DagentContainerConfig dagent = 3; + optional CraneContainerConfig crane = 4; - /* Runtime info and requirements of a container */ - optional string runtimeConfig = 7; - optional string registry = 8; - string imageName = 9; - string tag = 10; + optional string registry = 5; + string imageName = 6; + string tag = 7; - optional RegistryAuth registryAuth = 11; + optional RegistryAuth registryAuth = 8; } message ContainerStateRequest { diff --git a/web/crux/proto/common.proto b/web/crux/proto/common.proto index 35c6114c61..98f5aa2c53 100644 --- a/web/crux/proto/common.proto +++ b/web/crux/proto/common.proto @@ -187,12 +187,17 @@ message KeyValue { string value = 101; } +message ContainerOrPrefix { + oneof target { + common.ContainerIdentifier container = 1; + string prefix = 2; + } +} + message ListSecretsResponse { - string prefix = 1; - string name = 2; + ContainerOrPrefix target = 1; string publicKey = 3; - bool hasKeys = 4; - repeated string keys = 5; + repeated string keys = 4; } message UniqueKey { @@ -218,8 +223,5 @@ message ContainerCommandRequest { } message DeleteContainersRequest { - oneof target { - common.ContainerIdentifier container = 201; - string prefix = 202; - } + ContainerOrPrefix target = 1; } diff --git a/web/crux/src/app/agent/agent.connection-strategy.provider.ts b/web/crux/src/app/agent/agent.connection-strategy.provider.ts index 64e79f24df..db469506a7 100644 --- a/web/crux/src/app/agent/agent.connection-strategy.provider.ts +++ b/web/crux/src/app/agent/agent.connection-strategy.provider.ts @@ -1,11 +1,10 @@ import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common' import { JwtService } from '@nestjs/jwt' import { AgentToken } from 'src/domain/agent-token' -import { CruxUnauthorizedException } from 'src/exception/crux-exception' +import { CruxBadRequestException, CruxUnauthorizedException } from 'src/exception/crux-exception' import GrpcNodeConnection from 'src/shared/grpc-node-connection' import AgentService from './agent.service' import AgentConnectionInstallStrategy from './connection-strategies/agent.connection.install.strategy' -import AgentConnectionLegacyStrategy from './connection-strategies/agent.connection.legacy.strategy' import AgentConnectionStrategy from './connection-strategies/agent.connection.strategy' import AgentConnectionUpdateStrategy from './connection-strategies/agent.connection.update.strategy' @@ -18,7 +17,6 @@ export default class AgentConnectionStrategyProvider { private readonly service: AgentService, private readonly jwtService: JwtService, private readonly defaultStrategy: AgentConnectionStrategy, - private readonly legacy: AgentConnectionLegacyStrategy, private readonly install: AgentConnectionInstallStrategy, private readonly update: AgentConnectionUpdateStrategy, ) {} @@ -27,8 +25,10 @@ export default class AgentConnectionStrategyProvider { const token = this.jwtService.decode(connection.jwt) as AgentToken if (!token.version) { - this.logger.verbose(`${connection.nodeId} - No version found in the token. Using legacy strategy.`) - return this.legacy + this.logger.verbose(`${connection.nodeId} - No version found in the token. Declining connection.`) + throw new CruxBadRequestException({ + message: 'Legacy agents are not supported.', + }) } if (token.type === 'install') { @@ -60,7 +60,6 @@ export default class AgentConnectionStrategyProvider { export const AGENT_STRATEGY_TYPES = [ AgentConnectionStrategy, - AgentConnectionLegacyStrategy, AgentConnectionInstallStrategy, AgentConnectionUpdateStrategy, AgentConnectionStrategyProvider, diff --git a/web/crux/src/app/agent/agent.module.ts b/web/crux/src/app/agent/agent.module.ts index dda633a024..a4d0d3fc82 100644 --- a/web/crux/src/app/agent/agent.module.ts +++ b/web/crux/src/app/agent/agent.module.ts @@ -15,7 +15,13 @@ import AgentController from './agent.grpc.controller' import AgentService from './agent.service' @Module({ - imports: [HttpModule, CruxJwtModule, ImageModule, ContainerModule, forwardRef(() => DeployModule)], + imports: [ + HttpModule, + CruxJwtModule, + forwardRef(() => ImageModule), + forwardRef(() => ContainerModule), + forwardRef(() => DeployModule), + ], exports: [AgentService], controllers: [AgentController], providers: [ diff --git a/web/crux/src/app/agent/agent.service.ts b/web/crux/src/app/agent/agent.service.ts index a2c9fd2076..3860abcaf2 100755 --- a/web/crux/src/app/agent/agent.service.ts +++ b/web/crux/src/app/agent/agent.service.ts @@ -55,7 +55,6 @@ import DeployService from '../deploy/deploy.service' import { DagentTraefikOptionsDto, NodeConnectionStatus, NodeScriptTypeDto } from '../node/node.dto' import AgentConnectionStrategyProvider from './agent.connection-strategy.provider' import { AgentKickReason } from './agent.dto' -import AgentConnectionLegacyStrategy from './connection-strategies/agent.connection.legacy.strategy' @Injectable() export default class AgentService { @@ -536,15 +535,6 @@ export default class AgentService { const agent = await strategy.execute(connection, request) this.logger.verbose('Connection strategy completed') - if (agent.id === AgentConnectionLegacyStrategy.LEGACY_NONCE) { - // self destruct message is already in the queue - // we just have to return the command channel - - // command channel is already completed so no need for onDisconnected() call - this.logger.verbose('Crashing legacy agent intercepted.') - return agent.onConnected(AgentConnectionLegacyStrategy.CONNECTION_STATUS_LISTENER) - } - await this.onAgentConnectionStatusChange(agent, agent.outdated ? 'outdated' : 'connected') return agent.onConnected(it => this.onAgentConnectionStatusChange(agent, it)) diff --git a/web/crux/src/app/agent/connection-strategies/agent.connection.legacy.strategy.ts b/web/crux/src/app/agent/connection-strategies/agent.connection.legacy.strategy.ts deleted file mode 100644 index 3a77afb365..0000000000 --- a/web/crux/src/app/agent/connection-strategies/agent.connection.legacy.strategy.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Injectable } from '@nestjs/common' -import { Subject } from 'rxjs' -import { Agent } from 'src/domain/agent' -import { generateAgentToken } from 'src/domain/agent-token' -import { CruxConflictException } from 'src/exception/crux-exception' -import { AgentInfo, CloseReason } from 'src/grpc/protobuf/proto/agent' -import GrpcNodeConnection from 'src/shared/grpc-node-connection' -import AgentConnectionStrategy from './agent.connection.strategy' - -@Injectable() -export default class AgentConnectionLegacyStrategy extends AgentConnectionStrategy { - override async execute(connection: GrpcNodeConnection, info: AgentInfo): Promise { - const token = this.parseToken(connection, info) - const node = await this.findNodeById(token.sub) - - const connectedAgent = this.service.getById(node.id) - - if (node.token?.nonce === AgentConnectionLegacyStrategy.LEGACY_NONCE) { - if (this.service.agentVersionSupported(info.version)) { - // incoming updated agent with legacy token - const incomingAgent = await this.createAgent({ - connection, - info, - node, - outdated: false, - callbackTimeout: this.callbackTimeout, - }) - - if (connectedAgent) { - if (!connectedAgent.outdated) { - // duplicated connection - throw new CruxConflictException({ - message: 'Agent is already connected.', - property: 'id', - }) - } - - await this.deleteOldAgentContainer(connectedAgent, incomingAgent) - } - // generate new token for the now up to date agent - const replacement = this.service.generateConnectionTokenFor(incomingAgent.id, node.createdBy) - incomingAgent.replaceToken(replacement) - return incomingAgent - } - - this.throwIfConnected(node.id) - - // simple legacy agent - return await this.createAgent({ - connection, - info, - node, - outdated: true, - callbackTimeout: this.callbackTimeout, - }) - } - - // this legacy token is already replaced - // we send a shutdown to the incoming agent - info.id = AgentConnectionLegacyStrategy.LEGACY_NONCE - const legacyToken = generateAgentToken(AgentConnectionLegacyStrategy.LEGACY_NONCE, 'install') - const signedLegacyToken = this.jwtService.sign(legacyToken) - connection.onTokenReplaced(legacyToken, signedLegacyToken) - - const incomingAgent = new Agent({ - connection, - eventChannel: new Subject(), - info, - outdated: true, - callbackTimeout: this.callbackTimeout, - }) - - await this.deleteOldAgentContainer(incomingAgent, connectedAgent) - return incomingAgent - } - - private async deleteOldAgentContainer(oldAgent: Agent, newAgent: Agent): Promise { - this.logger.verbose('Sending shutdown to the outdated agent.') - oldAgent.close(CloseReason.SHUTDOWN) - - const containerName = newAgent?.info?.containerName - if (containerName) { - // remove the old agent's container - - this.logger.verbose("Removing old agent's container.") - await newAgent.deleteContainers({ - container: { - prefix: '', - name: `${containerName}-update`, - }, - }) - } - } - - static LEGACY_NONCE = '00000000-0000-0000-0000-000000000000' - - static CONNECTION_STATUS_LISTENER = () => {} -} diff --git a/web/crux/src/app/config.bundle/config.bundle.dto.ts b/web/crux/src/app/config.bundle/config.bundle.dto.ts index 0a0b2ef9b1..5ba21da51d 100644 --- a/web/crux/src/app/config.bundle/config.bundle.dto.ts +++ b/web/crux/src/app/config.bundle/config.bundle.dto.ts @@ -1,5 +1,5 @@ import { IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator' -import { UniqueKeyValueDto } from '../container/container.dto' +import { ContainerConfigDto } from '../container/container.dto' class BasicConfigBundleDto { @IsUUID() @@ -13,11 +13,14 @@ export class ConfigBundleDto extends BasicConfigBundleDto { @IsString() @IsOptional() description?: string + + @IsUUID() + configId: string } export class ConfigBundleDetailsDto extends ConfigBundleDto { - @ValidateNested({ each: true }) - environment: UniqueKeyValueDto[] + @ValidateNested() + config: ContainerConfigDto } export class CreateConfigBundleDto { @@ -38,9 +41,9 @@ export class PatchConfigBundleDto { @IsOptional() description?: string - @ValidateNested({ each: true }) + @ValidateNested() @IsOptional() - environment?: UniqueKeyValueDto[] + config?: ContainerConfigDto } export class ConfigBundleOptionDto extends BasicConfigBundleDto {} diff --git a/web/crux/src/app/config.bundle/config.bundle.http.controller.ts b/web/crux/src/app/config.bundle/config.bundle.http.controller.ts index bc8eb36d1d..2b2ec38368 100644 --- a/web/crux/src/app/config.bundle/config.bundle.http.controller.ts +++ b/web/crux/src/app/config.bundle/config.bundle.http.controller.ts @@ -6,8 +6,8 @@ import { HttpCode, HttpStatus, Param, + Patch, Post, - Put, UseGuards, UseInterceptors, } from '@nestjs/common' @@ -30,7 +30,6 @@ import { IdentityFromRequest } from '../token/jwt-auth.guard' import { ConfigBundleDetailsDto, ConfigBundleDto, - ConfigBundleOptionDto, CreateConfigBundleDto, PatchConfigBundleDto, } from './config.bundle.dto' @@ -69,24 +68,6 @@ export default class ConfigBundlesHttpController { return this.service.getConfigBundles(teamSlug) } - @Get('options') - @HttpCode(HttpStatus.OK) - @ApiOperation({ - description: 'Response should include `id`, and `name`.', - summary: 'Fetch the name and ID of available config bundle options.', - }) - @ApiOkResponse({ - type: ConfigBundleOptionDto, - isArray: true, - description: 'Name and ID of config bundle options listed.', - }) - @ApiBadRequestResponse({ description: 'Bad request for config bundle options.' }) - @ApiForbiddenResponse({ description: 'Unauthorized request for config bundle options.' }) - @ApiNotFoundResponse({ description: 'Config bundle options not found.' }) - async getConfigBundleOptions(@TeamSlug() teamSlug: string): Promise { - return this.service.getConfigBundleOptions(teamSlug) - } - @Get(ROUTE_CONFIG_BUNDLE_ID) @HttpCode(HttpStatus.OK) @ApiOperation({ @@ -128,10 +109,10 @@ export default class ConfigBundlesHttpController { } } - @Put(ROUTE_CONFIG_BUNDLE_ID) + @Patch(ROUTE_CONFIG_BUNDLE_ID) @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ - description: 'Updates a config bundle. Request must include `id`, `name`, and `data`', + description: 'Updates a config bundle.', summary: 'Modify a config bundle.', }) @ApiNoContentResponse({ description: 'Config bundle updated.' }) @@ -140,7 +121,7 @@ export default class ConfigBundlesHttpController { @ApiNotFoundResponse({ description: 'Config bundle not found.' }) @ApiConflictResponse({ description: 'Config bundle name taken.' }) @UuidParams(PARAM_CONFIG_BUNDLE_ID) - async updateConfigBundle( + async patchConfigBundle( @TeamSlug() _: string, @ConfigBundleId() id: string, @Body() request: PatchConfigBundleDto, diff --git a/web/crux/src/app/config.bundle/config.bundle.mapper.ts b/web/crux/src/app/config.bundle/config.bundle.mapper.ts index e68b5cadde..5436c5b4c1 100644 --- a/web/crux/src/app/config.bundle/config.bundle.mapper.ts +++ b/web/crux/src/app/config.bundle/config.bundle.mapper.ts @@ -1,22 +1,37 @@ -import { Injectable } from '@nestjs/common' -import { ConfigBundle } from '@prisma/client' -import { UniqueKeyValue } from 'src/domain/container' +import { forwardRef, Inject, Injectable } from '@nestjs/common' +import { ConfigBundle, ContainerConfig } from '@prisma/client' +import { ContainerConfigData } from 'src/domain/container' +import ContainerMapper from '../container/container.mapper' import { ConfigBundleDetailsDto, ConfigBundleDto } from './config.bundle.dto' @Injectable() export default class ConfigBundleMapper { - listItemToDto(configBundle: ConfigBundle): ConfigBundleDto { + constructor( + @Inject(forwardRef(() => ContainerMapper)) + private readonly containerMapper: ContainerMapper, + ) {} + + toDto(it: ConfigBundle): ConfigBundleDto { return { - id: configBundle.id, - name: configBundle.name, - description: configBundle.description, + id: it.id, + name: it.name, + description: it.description, + configId: it.configId, } } - detailsToDto(configBundle: ConfigBundle): ConfigBundleDetailsDto { + detailsToDto(configBundle: ConfigBundleDetails): ConfigBundleDetailsDto { return { - ...this.listItemToDto(configBundle), - environment: configBundle.data as UniqueKeyValue[], + ...this.toDto(configBundle), + config: this.containerMapper.configDataToDto( + configBundle.configId, + 'configBundle', + configBundle.config as any as ContainerConfigData, + ), } } } + +type ConfigBundleDetails = ConfigBundle & { + config: ContainerConfig +} diff --git a/web/crux/src/app/config.bundle/config.bundle.message.ts b/web/crux/src/app/config.bundle/config.bundle.message.ts deleted file mode 100644 index 884a6e8497..0000000000 --- a/web/crux/src/app/config.bundle/config.bundle.message.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UniqueKeyValue } from 'src/domain/container' - -export const WS_TYPE_PATCH_CONFIG_BUNDLE = 'patch-config-bundle' -export type PatchConfigBundleEnvMessage = { - name?: string - description?: string - environment?: UniqueKeyValue[] -} - -export const WS_TYPE_CONFIG_BUNDLE_UPDATED = 'config-bundle-updated' -export type ConfigBundleEnvUpdatedMessage = { - name?: string - description?: string - environment?: UniqueKeyValue[] -} - -export const WS_TYPE_PATCH_RECEIVED = 'patch-received' diff --git a/web/crux/src/app/config.bundle/config.bundle.module.ts b/web/crux/src/app/config.bundle/config.bundle.module.ts index bb8aaf90be..e6f070103e 100644 --- a/web/crux/src/app/config.bundle/config.bundle.module.ts +++ b/web/crux/src/app/config.bundle/config.bundle.module.ts @@ -1,27 +1,20 @@ import { HttpModule } from '@nestjs/axios' -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' import KratosService from 'src/services/kratos.service' import PrismaService from 'src/services/prisma.service' import AuditLoggerModule from '../audit.logger/audit.logger.module' +import ContainerModule from '../container/container.module' import EditorModule from '../editor/editor.module' import TeamModule from '../team/team.module' import TeamRepository from '../team/team.repository' import ConfigBundlesHttpController from './config.bundle.http.controller' import ConfigBundleMapper from './config.bundle.mapper' import ConfigBundleService from './config.bundle.service' -import ConfigBundleWebSocketGateway from './config.bundle.ws.gateway' @Module({ - imports: [HttpModule, TeamModule, AuditLoggerModule, EditorModule], + imports: [HttpModule, TeamModule, AuditLoggerModule, EditorModule, forwardRef(() => ContainerModule)], exports: [ConfigBundleMapper, ConfigBundleService], controllers: [ConfigBundlesHttpController], - providers: [ - ConfigBundleService, - PrismaService, - ConfigBundleMapper, - TeamRepository, - KratosService, - ConfigBundleWebSocketGateway, - ], + providers: [ConfigBundleService, PrismaService, ConfigBundleMapper, TeamRepository, KratosService], }) export default class ConfigBundleModule {} diff --git a/web/crux/src/app/config.bundle/config.bundle.service.ts b/web/crux/src/app/config.bundle/config.bundle.service.ts index 44667504e7..3bbeeb36ec 100644 --- a/web/crux/src/app/config.bundle/config.bundle.service.ts +++ b/web/crux/src/app/config.bundle/config.bundle.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common' import { Identity } from '@ory/kratos-client' -import { toPrismaJson } from 'src/domain/utils' import PrismaService from 'src/services/prisma.service' +import ContainerConfigService from '../container/container-config.service' import { EditorLeftMessage, EditorMessage } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' import TeamRepository from '../team/team.repository' @@ -19,9 +19,10 @@ export default class ConfigBundleService { private readonly logger = new Logger(ConfigBundleService.name) constructor( - private teamRepository: TeamRepository, - private prisma: PrismaService, - private mapper: ConfigBundleMapper, + private readonly teamRepository: TeamRepository, + private readonly prisma: PrismaService, + private readonly mapper: ConfigBundleMapper, + private readonly containerConfigService: ContainerConfigService, private readonly editorServices: EditorServiceProvider, ) {} @@ -34,7 +35,7 @@ export default class ConfigBundleService { }, }) - return configBundles.map(it => this.mapper.listItemToDto(it)) + return configBundles.map(it => this.mapper.toDto(it)) } async getConfigBundleDetails(id: string): Promise { @@ -42,6 +43,9 @@ export default class ConfigBundleService { where: { id, }, + include: { + config: true, + }, }) return this.mapper.detailsToDto(configBundle) @@ -58,16 +62,29 @@ export default class ConfigBundleService { data: { name: req.name, description: req.description, - data: [], - teamId, + config: { create: { type: 'configBundle' } }, + team: { connect: { id: teamId } }, createdBy: identity.id, }, + include: { + config: true, + }, }) return this.mapper.detailsToDto(configBundle) } async patchConfigBundle(id: string, req: PatchConfigBundleDto, identity: Identity): Promise { + if (req.config) { + await this.containerConfigService.patchConfig( + req.config.id, + { + config: req.config, + }, + identity, + ) + } + await this.prisma.configBundle.update({ where: { id, @@ -75,7 +92,6 @@ export default class ConfigBundleService { data: { name: req.name ?? undefined, description: req.description ?? undefined, - data: req.environment ? toPrismaJson(req.environment) : undefined, updatedBy: identity.id, }, }) diff --git a/web/crux/src/app/container/container-config.domain-event.listener.ts b/web/crux/src/app/container/container-config.domain-event.listener.ts new file mode 100644 index 0000000000..81f391d605 --- /dev/null +++ b/web/crux/src/app/container/container-config.domain-event.listener.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common' +import { OnEvent } from '@nestjs/event-emitter' +import { filter, Observable, Subject } from 'rxjs' +import { CONTAINER_CONFIG_EVENT_UPDATE, ContainerConfigUpdatedEvent } from 'src/domain/domain-events' +import { DomainEvent } from 'src/shared/domain-event' + +@Injectable() +export default class ContainerConfigDomainEventListener { + private readonly configUpdatedEvents = new Subject>() + + watchEvents(configId: string): Observable> { + return this.configUpdatedEvents.pipe(filter(it => it.event.id === configId)) + } + + @OnEvent(CONTAINER_CONFIG_EVENT_UPDATE) + onContainerConfigUpdatedEvent(event: ContainerConfigUpdatedEvent) { + this.configUpdatedEvents.next({ + type: CONTAINER_CONFIG_EVENT_UPDATE, + event, + }) + } +} diff --git a/web/crux/src/app/container/container-config.http.service.ts b/web/crux/src/app/container/container-config.http.service.ts new file mode 100644 index 0000000000..d0900a9732 --- /dev/null +++ b/web/crux/src/app/container/container-config.http.service.ts @@ -0,0 +1,84 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, UseGuards } from '@nestjs/common' +import { + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiNoContentResponse, + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger' +import { Identity } from '@ory/kratos-client' +import UuidParams from 'src/decorators/api-params.decorator' +import { IdentityFromRequest } from '../token/jwt-auth.guard' +import ContainerConfigService from './container-config.service' +import { ContainerConfigDto, ContainerConfigRelationsDto, PatchContainerConfigDto } from './container.dto' +import ContainerConfigTeamAccessGuard from './guards/container-config.team-access.guard' + +const PARAM_TEAM_SLUG = 'teamSlug' +const TeamSlug = () => Param(PARAM_TEAM_SLUG) +const PARAM_CONFIG_ID = 'configId' +const ConfigId = () => Param(PARAM_CONFIG_ID) + +const ROUTE_TEAM_SLUG = ':teamSlug' +const ROUTE_CONTAINER_CONFIGS = 'container-configurations' +const ROUTE_CONFIG_ID = ':configId' + +@Controller(`${ROUTE_TEAM_SLUG}/${ROUTE_CONTAINER_CONFIGS}`) +@ApiTags(ROUTE_CONTAINER_CONFIGS) +@UseGuards(ContainerConfigTeamAccessGuard) +export default class ContainerConfigHttpController { + constructor(private service: ContainerConfigService) {} + + @Get(ROUTE_CONFIG_ID) + @HttpCode(HttpStatus.OK) + @ApiOperation({ + description: 'Get details of a container configuration. Request must include `teamSlug` and `configId` in URL.', + summary: 'Retrieve details of a container configuration.', + }) + @ApiOkResponse({ type: ContainerConfigDto, description: 'Details of a container configuration.' }) + @ApiBadRequestResponse({ description: 'Bad request for container configuration details.' }) + @ApiForbiddenResponse({ description: 'Unauthorized request for container configuration details.' }) + @ApiNotFoundResponse({ description: 'Container configuration not found.' }) + @UuidParams(PARAM_CONFIG_ID) + async getConfigDetails(@TeamSlug() _: string, @ConfigId() configId: string): Promise { + return await this.service.getConfigDetails(configId) + } + + @Get(`${ROUTE_CONFIG_ID}/relations`) + @HttpCode(HttpStatus.OK) + @ApiOperation({ + description: + 'Get the relations of a container configuration. Request must include `teamSlug` and `configId` in URL.', + summary: 'Retrieve the relations of a container configuration.', + }) + @ApiOkResponse({ type: ContainerConfigRelationsDto, description: 'Relations of a container configuration.' }) + @ApiBadRequestResponse({ description: 'Bad request for container configuration relations.' }) + @ApiForbiddenResponse({ description: 'Unauthorized request for container configuration relations.' }) + @ApiNotFoundResponse({ description: 'Container configuration not found.' }) + @UuidParams(PARAM_CONFIG_ID) + async getConfigRelations(@TeamSlug() _: string, @ConfigId() configId: string): Promise { + return await this.service.getConfigRelations(configId) + } + + @Patch(ROUTE_CONFIG_ID) + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + description: 'Request must include `configId` and `teamSlug` in URL.', + summary: 'Update container configuration.', + }) + // @UseInterceptors(DeployPatchValidationInterceptor) + @ApiNoContentResponse({ description: 'Container configuration modified.' }) + @ApiBadRequestResponse({ description: 'Bad request for a container configuration.' }) + @ApiForbiddenResponse({ description: 'Unauthorized request for a container configuration.' }) + @ApiNotFoundResponse({ description: 'Container configuration not found.' }) + @UuidParams(PARAM_CONFIG_ID) + async patchDeployment( + @TeamSlug() _: string, + @ConfigId() configId: string, + @Body() request: PatchContainerConfigDto, + @IdentityFromRequest() identity: Identity, + ): Promise { + await this.service.patchConfig(configId, request, identity) + } +} diff --git a/web/crux/src/app/container/container-config.message.ts b/web/crux/src/app/container/container-config.message.ts new file mode 100644 index 0000000000..584f40fd1d --- /dev/null +++ b/web/crux/src/app/container/container-config.message.ts @@ -0,0 +1,22 @@ +import { ContainerConfigData } from 'src/domain/container' +import { ContainerConfigProperty } from './container.const' + +export const WS_TYPE_GET_CONFIG_SECRETS = 'get-config-secrets' +export const WS_TYPE_CONFIG_SECRETS = 'config-secrets' +export type ConfigSecretsMessage = { + keys: string[] + publicKey: string +} + +export const WS_TYPE_PATCH_CONFIG = 'patch-config' +export type PatchConfigMessage = { + config?: ContainerConfigData + resetSection?: ContainerConfigProperty +} + +export const WS_TYPE_PATCH_RECEIVED = 'patch-received' + +export const WS_TYPE_CONFIG_UPDATED = 'config-updated' +export type ConfigUpdatedMessage = ContainerConfigData & { + id: string +} diff --git a/web/crux/src/app/container/container-config.service.ts b/web/crux/src/app/container/container-config.service.ts new file mode 100644 index 0000000000..0c736e400e --- /dev/null +++ b/web/crux/src/app/container/container-config.service.ts @@ -0,0 +1,391 @@ +import { Injectable, Logger } from '@nestjs/common' +import { EventEmitter2 } from '@nestjs/event-emitter' +import { Identity } from '@ory/kratos-client' +import { Prisma } from '@prisma/client' +import { Observable, filter, map } from 'rxjs' +import { ContainerConfigData, nameOfInstance } from 'src/domain/container' +import { deploymentIsMutable } from 'src/domain/deployment' +import { CONTAINER_CONFIG_EVENT_UPDATE, ContainerConfigUpdatedEvent } from 'src/domain/domain-events' +import { versionIsMutable } from 'src/domain/version' +import { CruxBadRequestException, CruxPreconditionFailedException } from 'src/exception/crux-exception' +import PrismaService from 'src/services/prisma.service' +import { DomainEvent } from 'src/shared/domain-event' +import { WsMessage } from 'src/websockets/common' +import AgentService from '../agent/agent.service' +import { EditorLeftMessage, EditorMessage } from '../editor/editor.message' +import EditorServiceProvider from '../editor/editor.service.provider' +import ContainerConfigDomainEventListener from './container-config.domain-event.listener' +import { ConfigUpdatedMessage, WS_TYPE_CONFIG_UPDATED } from './container-config.message' +import { + ContainerConfigDetailsDto, + ContainerConfigRelationsDto, + ContainerSecretsDto, + PatchContainerConfigDto, +} from './container.dto' +import ContainerMapper from './container.mapper' + +@Injectable() +export default class ContainerConfigService { + private readonly logger = new Logger(ContainerConfigService.name) + + constructor( + private readonly prisma: PrismaService, + private readonly mapper: ContainerMapper, + private readonly agentService: AgentService, + private readonly editorServices: EditorServiceProvider, + private readonly domainEventListener: ContainerConfigDomainEventListener, + private readonly events: EventEmitter2, + ) {} + + async checkConfigIsInTeam(teamSlug: string, configId: string, identity: Identity): Promise { + const teamWhere: Prisma.TeamWhereInput = { + slug: teamSlug, + users: { + some: { + userId: identity.id, + }, + }, + } + + const versionWhere: Prisma.VersionWhereInput = { + project: { + team: teamWhere, + }, + } + + const deploymentWhere: Prisma.DeploymentWhereInput = { + version: versionWhere, + } + + const configs = await this.prisma.containerConfig.count({ + where: { + id: configId, + OR: [ + { + image: { + version: versionWhere, + }, + }, + { + instance: { + deployment: deploymentWhere, + }, + }, + { + deployment: deploymentWhere, + }, + { + configBundle: { + team: teamWhere, + }, + }, + ], + }, + }) + + return configs > 0 + } + + subscribeToDomainEvents(configId: string): Observable { + return this.domainEventListener.watchEvents(configId).pipe( + map(it => this.transformDomainEventToWsMessage(it)), + filter(it => !!it), + ) + } + + async getConfigDetails(configId: string): Promise { + const config = await this.prisma.containerConfig.findUniqueOrThrow({ + where: { + id: configId, + }, + include: { + image: { + include: { + version: { + include: { + children: true, + deployments: { + select: { + status: true, + }, + }, + }, + }, + }, + }, + configBundle: true, + deployment: { + include: { + version: true, + }, + }, + instance: { + include: { + image: true, + deployment: { + include: { + version: true, + }, + }, + }, + }, + }, + }) + + return this.mapper.configDetailsToDto(config) + } + + async getConfigSecrets(configId: string): Promise { + const config = await this.prisma.containerConfig.findUnique({ + where: { + id: configId, + }, + select: { + type: true, + secrets: true, + instance: { + select: { + deployment: true, + config: { + select: { + name: true, + }, + }, + image: { + select: { + name: true, + config: { + select: { + name: true, + }, + }, + }, + }, + }, + }, + deployment: true, + }, + }) + + const deployment = config.type === 'instance' ? config.instance.deployment : config.deployment + if (!deployment) { + return null + } + + const agent = this.agentService.getById(deployment.nodeId) + if (!agent) { + throw new CruxPreconditionFailedException({ + message: 'Node is unreachable', + property: 'nodeId', + value: deployment.nodeId, + }) + } + + const secrets = await agent.listSecrets({ + target: + config.type === 'deployment' + ? { + prefix: deployment.prefix, + } + : { + container: { + prefix: deployment.prefix, + name: nameOfInstance(config.instance), + }, + }, + }) + + return this.mapper.secretsResponseToDto(secrets) + } + + async getConfigRelations(configId: string): Promise { + const versionInclude = { + include: { + project: true, + }, + } + + const deploymentInclude = { + include: { + version: versionInclude, + config: true, + node: true, + configBundles: { + select: { + configBundle: { + include: { + config: true, + }, + }, + }, + }, + }, + } + + const config = await this.prisma.containerConfig.findUnique({ + where: { + id: configId, + }, + select: { + type: true, + configBundle: true, + deployment: deploymentInclude, + image: { + include: { + registry: true, + version: versionInclude, + config: true, + }, + }, + instance: { + include: { + image: { include: { registry: true, config: true } }, + deployment: deploymentInclude, + }, + }, + }, + }) + + return this.mapper.configRelationsToDto(config) + } + + async patchConfig(configId: string, req: PatchContainerConfigDto, identity: Identity): Promise { + const mutable = await this.checkMutability(configId) + if (!mutable) { + throw new CruxBadRequestException({ + message: 'Container config is immutable', + property: 'configId', + value: configId, + }) + } + + const config = await this.prisma.containerConfig.findUnique({ + where: { + id: configId, + }, + }) + + const data: ContainerConfigData = this.mapper.configDtoToConfigData( + config as any as ContainerConfigData, + req.config ?? {}, + ) + + if (req.resetSection) { + data[req.resetSection] = null + } + + await this.prisma.containerConfig.update({ + where: { + id: configId, + }, + data: { + ...this.mapper.configDataToDbPatch(data), + updatedBy: identity.id, + }, + }) + + await this.events.emitAsync(CONTAINER_CONFIG_EVENT_UPDATE, { + id: configId, + patch: data, + } as ContainerConfigUpdatedEvent) + + return { + id: configId, + ...data, + } + } + + async onEditorJoined( + configId: string, + clientToken: string, + identity: Identity, + ): Promise<[EditorMessage, EditorMessage[]]> { + const editors = await this.editorServices.getOrCreateService(configId) + + const me = editors.onClientJoin(clientToken, identity) + + return [me, editors.getEditors()] + } + + async onEditorLeft(configId: string, clientToken: string): Promise { + const editors = await this.editorServices.getOrCreateService(configId) + const message = editors.onClientLeft(clientToken) + + if (editors.editorCount < 1) { + this.logger.verbose(`All editors left removing ${configId}`) + this.editorServices.free(configId) + } + + return message + } + + private async checkMutability(configId: string): Promise { + const deploymentSelect: Prisma.DeploymentSelect = { + status: true, + version: { + select: { + type: true, + }, + }, + } + + const config = await this.prisma.containerConfig.findUnique({ + where: { + id: configId, + }, + select: { + type: true, + image: { + select: { + version: { + select: { + id: true, + type: true, + children: { + select: { + versionId: true, + }, + }, + deployments: { + select: { + status: true, + }, + }, + }, + }, + }, + }, + instance: { select: { deployment: { select: deploymentSelect } } }, + deployment: { select: deploymentSelect }, + configBundle: {}, + }, + }) + + switch (config.type) { + case 'image': + return versionIsMutable(config.image.version) + case 'deployment': + return deploymentIsMutable(config.deployment.status, config.deployment.version.type) + case 'instance': + return deploymentIsMutable(config.instance.deployment.status, config.instance.deployment.version.type) + case 'configBundle': + return true + default: + return false + } + } + + private transformDomainEventToWsMessage(ev: DomainEvent): WsMessage { + switch (ev.type) { + case CONTAINER_CONFIG_EVENT_UPDATE: + return { + type: WS_TYPE_CONFIG_UPDATED, + data: this.mapper.configUpdatedEventToMessage(ev.event as ContainerConfigUpdatedEvent), + } + default: { + this.logger.error(`Unhandled domain event ${ev.type}`) + return null + } + } + } +} diff --git a/web/crux/src/app/config.bundle/config.bundle.ws.gateway.ts b/web/crux/src/app/container/container-config.ws.gateway.ts similarity index 67% rename from web/crux/src/app/config.bundle/config.bundle.ws.gateway.ts rename to web/crux/src/app/container/container-config.ws.gateway.ts index f7da49363d..f8e5ac9f56 100644 --- a/web/crux/src/app/config.bundle/config.bundle.ws.gateway.ts +++ b/web/crux/src/app/container/container-config.ws.gateway.ts @@ -1,5 +1,6 @@ import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' import { Identity } from '@ory/kratos-client' +import { takeUntil } from 'rxjs' import { AuditLogLevel } from 'src/decorators/audit-logger.decorator' import { WsAuthorize, WsClient, WsMessage, WsSubscribe, WsSubscription, WsUnsubscribe } from 'src/websockets/common' import SocketClient from 'src/websockets/decorators/ws.client.decorator' @@ -26,48 +27,54 @@ import { } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' import { IdentityFromSocket } from '../token/jwt-auth.guard' -import { PatchConfigBundleDto } from './config.bundle.dto' import { - ConfigBundleEnvUpdatedMessage, - PatchConfigBundleEnvMessage, - WS_TYPE_CONFIG_BUNDLE_UPDATED, - WS_TYPE_PATCH_CONFIG_BUNDLE, + ConfigSecretsMessage, + PatchConfigMessage, + WS_TYPE_CONFIG_SECRETS, + WS_TYPE_GET_CONFIG_SECRETS, + WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_RECEIVED, -} from './config.bundle.message' -import ConfigBundleService from './config.bundle.service' +} from './container-config.message' +import ContainerConfigService from './container-config.service' const TeamSlug = () => WsParam('teamSlug') -const ConfigBundleId = () => WsParam('configBundleId') +const ConfigId = () => WsParam('configId') @WebSocketGateway({ - namespace: ':teamSlug/config-bundles/:configBundleId', + namespace: ':teamSlug/container-configurations/:configId', }) @UseGlobalWsFilters() @UseGlobalWsGuards() @UseGlobalWsInterceptors() -export default class ConfigBundleWebSocketGateway { +export default class ContainerConfigWebSocketGateway { constructor( - private readonly service: ConfigBundleService, + private readonly service: ContainerConfigService, private readonly editorServices: EditorServiceProvider, ) {} @WsAuthorize() async onAuthorize( @TeamSlug() teamSlug: string, - @ConfigBundleId() configBundleId: string, + @ConfigId() configId: string, @IdentityFromSocket() identity: Identity, ): Promise { - return await this.service.checkConfigBundleIsInTeam(teamSlug, configBundleId, identity) + return await this.service.checkConfigIsInTeam(teamSlug, configId, identity) } @WsSubscribe() async onSubscribe( @SocketClient() client: WsClient, - @ConfigBundleId() configBundleId: string, + @ConfigId() configId: string, @IdentityFromSocket() identity, @SocketSubscription() subscription: WsSubscription, ): Promise> { - const [me, editors] = await this.service.onEditorJoined(configBundleId, client.token, identity) + const [me, editors] = await this.service.onEditorJoined(configId, client.token, identity) + + this.service + .subscribeToDomainEvents(configId) + .pipe(takeUntil(subscription.getCompleter(client.token))) + .subscribe(message => subscription.sendToAll(message)) + subscription.sendToAllExcept(client, { type: WS_TYPE_EDITOR_JOINED, data: me, @@ -85,35 +92,25 @@ export default class ConfigBundleWebSocketGateway { @WsUnsubscribe() async onUnsubscribe( @SocketClient() client: WsClient, - @ConfigBundleId() configBundleId: string, + @ConfigId() configId: string, @SocketSubscription() subscription: WsSubscription, ): Promise { - const data = await this.service.onEditorLeft(configBundleId, client.token) + const data = await this.service.onEditorLeft(configId, client.token) const message: WsMessage = { type: WS_TYPE_EDITOR_LEFT, data, } + subscription.sendToAllExcept(client, message) } - @SubscribeMessage(WS_TYPE_PATCH_CONFIG_BUNDLE) - async patchConfigBundleEnvironment( - @ConfigBundleId() configBundleId: string, - @SocketMessage() message: PatchConfigBundleEnvMessage, + @SubscribeMessage(WS_TYPE_PATCH_CONFIG) + async patchConfig( + @ConfigId() configId: string, + @SocketMessage() message: PatchConfigMessage, @IdentityFromSocket() identity: Identity, - @SocketClient() client: WsClient, - @SocketSubscription() subscription: WsSubscription, ): Promise> { - const cruxReq: PatchConfigBundleDto = { - ...message, - } - - await this.service.patchConfigBundle(configBundleId, cruxReq, identity) - - subscription.sendToAllExcept(client, { - type: WS_TYPE_CONFIG_BUNDLE_UPDATED, - data: message, - } as WsMessage) + await this.service.patchConfig(configId, message, identity) return { type: WS_TYPE_PATCH_RECEIVED, @@ -121,15 +118,28 @@ export default class ConfigBundleWebSocketGateway { } } + @SubscribeMessage(WS_TYPE_GET_CONFIG_SECRETS) + async getConfigSecrets(@ConfigId() configId: string): Promise> { + const secrets = await this.service.getConfigSecrets(configId) + + return { + type: WS_TYPE_CONFIG_SECRETS, + data: secrets ?? { + publicKey: null, + keys: [], + }, + } + } + @AuditLogLevel('disabled') @SubscribeMessage(WS_TYPE_FOCUS_INPUT) async onFocusInput( @SocketClient() client: WsClient, - @ConfigBundleId() configBundleId: string, + @ConfigId() configId: string, @SocketMessage() message: InputFocusMessage, @SocketSubscription() subscription: WsSubscription, ): Promise { - const editors = await this.editorServices.getService(configBundleId) + const editors = await this.editorServices.getService(configId) if (!editors) { return } @@ -148,11 +158,11 @@ export default class ConfigBundleWebSocketGateway { @SubscribeMessage(WS_TYPE_BLUR_INPUT) async onBlurInput( @SocketClient() client: WsClient, - @ConfigBundleId() configBundleId: string, + @ConfigId() configId: string, @SocketMessage() message: InputFocusMessage, @SocketSubscription() subscription: WsSubscription, ): Promise { - const editors = await this.editorServices.getService(configBundleId) + const editors = await this.editorServices.getService(configId) if (!editors) { return } diff --git a/web/crux/src/app/image/image.const.ts b/web/crux/src/app/container/container.const.ts similarity index 86% rename from web/crux/src/app/image/image.const.ts rename to web/crux/src/app/container/container.const.ts index 6af3725380..2eefc9f9ef 100644 --- a/web/crux/src/app/image/image.const.ts +++ b/web/crux/src/app/container/container.const.ts @@ -1,5 +1,3 @@ -// TODO: move this to the container domain - export const COMMON_CONFIG_PROPERTIES = [ 'name', 'environment', @@ -44,4 +42,4 @@ export const ALL_CONFIG_PROPERTIES = [ ...DAGENT_CONFIG_PROPERTIES, ] as const -export type ImageConfigProperty = (typeof ALL_CONFIG_PROPERTIES)[number] +export type ContainerConfigProperty = (typeof ALL_CONFIG_PROPERTIES)[number] diff --git a/web/crux/src/app/container/container.dto.ts b/web/crux/src/app/container/container.dto.ts index 57911d52fb..1451cdbf45 100644 --- a/web/crux/src/app/container/container.dto.ts +++ b/web/crux/src/app/container/container.dto.ts @@ -1,6 +1,8 @@ -import { ApiProperty, PartialType } from '@nestjs/swagger' +import { ApiProperty, OmitType } from '@nestjs/swagger' +import { Type } from 'class-transformer' import { IsBoolean, + IsDate, IsIn, IsInt, IsNumber, @@ -29,7 +31,16 @@ import { PORT_MAX, PORT_MIN, } from 'src/domain/container' -import { UID_MAX } from 'src/shared/const' +import { UID_MAX, UID_MIN } from 'src/shared/const' +import { ConfigBundleDto } from '../config.bundle/config.bundle.dto' +import { DeploymentWithConfigDto } from '../deploy/deploy.dto' +import { ImageDto } from '../image/image.dto' +import { BasicProjectDto } from '../project/project.dto' +import { BasicVersionDto } from '../version/version.dto' +import { ContainerConfigProperty } from './container.const' + +export const CONTAINER_CONFIG_TYPE_VALUES = ['image', 'instance', 'deployment', 'config-bundle'] as const +export type ContainerConfigTypeDto = (typeof CONTAINER_CONFIG_TYPE_VALUES)[number] export class UniqueKeyDto { @IsUUID() @@ -304,9 +315,18 @@ export class ExpectedContainerStateDto { } export class ContainerConfigDto { + @IsString() + id: string + + @ApiProperty({ enum: CONTAINER_CONFIG_TYPE_VALUES }) + @IsIn(CONTAINER_CONFIG_TYPE_VALUES) + @IsOptional() + type: ContainerConfigTypeDto + // common @IsString() - name: string + @IsOptional() + name?: string @IsOptional() @ValidateNested({ each: true }) @@ -322,11 +342,12 @@ export class ContainerConfigDto { @ApiProperty({ enum: CONTAINER_EXPOSE_STRATEGY_VALUES }) @IsIn(CONTAINER_EXPOSE_STRATEGY_VALUES) - expose: ContainerExposeStrategy + @IsOptional() + expose?: ContainerExposeStrategy @IsOptional() @IsInt() - @Min(-1) + @Min(UID_MIN) @Max(UID_MAX) user?: number @@ -335,7 +356,8 @@ export class ContainerConfigDto { workingDirectory?: string @IsBoolean() - tty: boolean + @IsOptional() + tty?: boolean @IsOptional() @ValidateNested() @@ -380,11 +402,13 @@ export class ContainerConfigDto { @ApiProperty({ enum: CONTAINER_RESTART_POLICY_TYPE_VALUES }) @IsIn(CONTAINER_RESTART_POLICY_TYPE_VALUES) - restartPolicy: ContainerRestartPolicyType + @IsOptional() + restartPolicy?: ContainerRestartPolicyType @ApiProperty({ enum: CONTAINER_NETWORK_MODE_VALUES }) @IsIn(CONTAINER_NETWORK_MODE_VALUES) - networkMode: ContainerNetworkMode + @IsOptional() + networkMode?: ContainerNetworkMode @IsOptional() @ValidateNested({ each: true }) @@ -401,17 +425,20 @@ export class ContainerConfigDto { // crane @ApiProperty({ enum: CONTAINER_DEPLOYMENT_STRATEGY_VALUES }) @IsIn(CONTAINER_DEPLOYMENT_STRATEGY_VALUES) - deploymentStrategy: ContainerDeploymentStrategyType + @IsOptional() + deploymentStrategy?: ContainerDeploymentStrategyType @IsOptional() @ValidateNested({ each: true }) customHeaders?: UniqueKeyDto[] @IsBoolean() - proxyHeaders: boolean + @IsOptional() + proxyHeaders?: boolean @IsBoolean() - useLoadBalancer: boolean + @IsOptional() + useLoadBalancer?: boolean @IsOptional() @ValidateNested({ each: true }) @@ -438,7 +465,72 @@ export class ContainerConfigDto { metrics?: MetricsDto } -export class PartialContainerConfigDto extends PartialType(ContainerConfigDto) {} +export class ContainerConfigRelationsDto { + @ValidateNested() + @IsOptional() + project?: BasicProjectDto + + @ValidateNested() + @IsOptional() + version?: BasicVersionDto + + @ValidateNested() + @IsOptional() + image?: ImageDto + + @ValidateNested() + @IsOptional() + deployment?: DeploymentWithConfigDto + + @ValidateNested() + @IsOptional() + configBundle?: ConfigBundleDto +} + +export class ContainerConfigParentDto { + @IsUUID() + id: string + + @IsString() + name: string + + @IsBoolean() + mutable: boolean +} + +export class ContainerConfigDetailsDto extends ContainerConfigDto { + @ValidateNested() + parent: ContainerConfigParentDto + + @Type(() => Date) + @IsDate() + @IsOptional() + updatedAt?: Date + + @IsString() + @IsOptional() + updatedBy?: string +} + +export class ContainerConfigDataDto extends OmitType(ContainerConfigDto, ['id', 'type']) {} + +export class PatchContainerConfigDto { + @IsOptional() + @ValidateNested() + config?: ContainerConfigDataDto + + @IsOptional() + @IsString() + resetSection?: ContainerConfigProperty +} + +export class ConcreteContainerConfigDto extends OmitType(ContainerConfigDto, ['secrets']) { + @IsOptional() + @ValidateNested({ each: true }) + secrets?: UniqueSecretKeyValueDto[] +} + +export class ConcreteContainerConfigDataDto extends OmitType(ConcreteContainerConfigDto, ['id', 'type']) {} export class ContainerIdentifierDto { @IsString() @@ -447,3 +539,11 @@ export class ContainerIdentifierDto { @IsString() name: string } + +export class ContainerSecretsDto { + @IsString() + publicKey: string + + @IsString({ each: true }) + keys: string[] +} diff --git a/web/crux/src/app/container/container.mapper.ts b/web/crux/src/app/container/container.mapper.ts index 3131051e2f..20fbe453ff 100644 --- a/web/crux/src/app/container/container.mapper.ts +++ b/web/crux/src/app/container/container.mapper.ts @@ -1,26 +1,69 @@ -import { Injectable } from '@nestjs/common' -import { ContainerConfig } from '@prisma/client' +import { forwardRef, Inject, Injectable } from '@nestjs/common' import { - ContainerConfigData, - InstanceContainerConfigData, - MergedContainerConfigData, - Metrics, - UniqueKeyValue, - UniqueSecretKey, - UniqueSecretKeyValue, -} from 'src/domain/container' -import { toPrismaJson } from 'src/domain/utils' -import { ContainerConfigDto, PartialContainerConfigDto, UniqueKeyValueDto } from './container.dto' + ConfigBundle, + ContainerConfig, + ContainerConfigType, + Deployment, + DeploymentStatusEnum, + Image, + Project, + Version, + VersionsOnParentVersion, +} from '@prisma/client' +import { ContainerConfigData } from 'src/domain/container' +import { deploymentIsMutable, DeploymentWithConfigAndBundles } from 'src/domain/deployment' +import { ContainerConfigUpdatedEvent } from 'src/domain/domain-events' +import { ImageDetails } from 'src/domain/image' +import { toNullableBoolean, toNullableNumber, toPrismaJson } from 'src/domain/utils' +import { versionIsMutable } from 'src/domain/version' +import { ListSecretsResponse } from 'src/grpc/protobuf/proto/common' +import ConfigBundleMapper from '../config.bundle/config.bundle.mapper' +import DeployMapper from '../deploy/deploy.mapper' +import ImageMapper from '../image/image.mapper' +import ProjectMapper from '../project/project.mapper' +import VersionMapper from '../version/version.mapper' +import { ConfigUpdatedMessage } from './container-config.message' +import { + ContainerConfigDataDto, + ContainerConfigDetailsDto, + ContainerConfigDto, + ContainerConfigParentDto, + ContainerConfigRelationsDto, + ContainerConfigTypeDto, + ContainerSecretsDto, +} from './container.dto' @Injectable() export default class ContainerMapper { - uniqueKeyValueDtoToDb(it: UniqueKeyValueDto): UniqueKeyValue { - return it + constructor( + private readonly projectMapper: ProjectMapper, + private readonly versionMapper: VersionMapper, + @Inject(forwardRef(() => ImageMapper)) + private readonly imageMapper: ImageMapper, + @Inject(forwardRef(() => DeployMapper)) + private readonly deployMapper: DeployMapper, + @Inject(forwardRef(() => ConfigBundleMapper)) + private readonly configBundleMapper: ConfigBundleMapper, + ) {} + + typeToDto(type: ContainerConfigType): ContainerConfigTypeDto { + switch (type) { + case 'configBundle': + return 'config-bundle' + default: + return type + } } - configDataToDto(config: ContainerConfigData): ContainerConfigDto { + configDataToDto(id: string, type: ContainerConfigType, config: ContainerConfigData): ContainerConfigDto { + if (!config) { + return null + } + return { ...config, + id, + type: this.typeToDto(type), capabilities: null, storage: !config.storageSet ? null @@ -32,51 +75,119 @@ export default class ContainerMapper { } } - configDtoToConfigData(current: ContainerConfigData, patch: PartialContainerConfigDto): ContainerConfigData { - const storagePatch = - 'storage' in patch - ? { - storageSet: !!patch.storage?.storageId, - storageId: patch.storage?.storageId ?? null, - storageConfig: patch.storage?.storageId - ? { - path: patch.storage.path, - bucket: patch.storage.bucket, - } - : null, - } - : undefined + configDetailsToDto(config: ContainerConfigDetails): ContainerConfigDetailsDto { + return { + ...this.configDataToDto(config.id, config.type, config as any as ContainerConfigData), + parent: this.configDetailsToParentDto(config), + updatedAt: config.updatedAt, + updatedBy: config.updatedBy, + } + } + + configRelationsToDto(config: ContainerConfigRelations): ContainerConfigRelationsDto { + switch (config.type) { + case 'image': { + const { version } = config.image + return { + image: this.imageMapper.toDetailsDto(config.image), + project: this.projectMapper.toBasicDto(version.project), + version: this.versionMapper.toBasicDto(version), + } + } + case 'instance': { + const { deployment } = config.instance + const { version } = deployment + + return { + image: this.imageMapper.toDetailsDto(config.instance.image), + project: this.projectMapper.toBasicDto(version.project), + version: this.versionMapper.toBasicDto(version), + deployment: this.deployMapper.toDeploymentWithConfigDto(deployment), + } + } + case 'deployment': { + const { deployment } = config + const { version } = deployment + + return { + project: this.projectMapper.toBasicDto(version.project), + version: this.versionMapper.toBasicDto(version), + deployment: this.deployMapper.toDeploymentWithConfigDto(deployment), + } + } + case 'configBundle': + return { + configBundle: this.configBundleMapper.toDto(config.configBundle), + } + default: + throw new Error(`Unknown ContainerConfigType ${config.type}`) + } + } + + secretsResponseToDto(secrets: ListSecretsResponse): ContainerSecretsDto { return { + keys: secrets.keys ?? [], + publicKey: secrets.publicKey, + } + } + + configDtoToConfigData(current: ContainerConfigData, patch: ContainerConfigDataDto): ContainerConfigData { + let result: ContainerConfigData = { ...current, ...patch, - capabilities: undefined, // TODO (@m8vago, @nandor-magyar): Remove this line, when capabilites are ready - annotations: !patch.annotations - ? current.annotations - : { - ...(current.annotations ?? {}), - ...patch.annotations, - }, - labels: !patch.labels - ? current.labels - : { - ...(current.labels ?? {}), - ...patch.labels, - }, - ...storagePatch, } + + if ('storage' in patch) { + result = { + ...result, + storageSet: true, + storageId: patch.storage?.storageId ?? null, + storageConfig: patch.storage?.storageId + ? { + path: patch.storage.path, + bucket: patch.storage.bucket, + } + : null, + } + } + + if ('annotations' in patch) { + result = { + ...result, + annotations: { + ...(current.annotations ?? {}), + ...patch.annotations, + }, + } + } + + if ('labels' in patch) { + result = { + ...result, + labels: { + ...(current.labels ?? {}), + ...patch.labels, + }, + } + } + + return result } - configDataToDb(config: Partial): Omit { + dbConfigToCreateConfigStatement( + config: Omit, + ): Omit { return { - name: config.name ?? undefined, - expose: config.expose ?? undefined, + type: config.type, + // common + name: config.name ?? null, + expose: config.expose ?? null, routing: toPrismaJson(config.routing), - configContainer: toPrismaJson(config.configContainer), - // Set user to the given value, if not null or use 0 if specifically 0, otherwise set to default -1 - user: config.user ?? (config.user === 0 ? 0 : -1), - workingDirectory: config.workingDirectory ?? undefined, - tty: config.tty !== null ? config.tty : false, + configContainer: toPrismaJson(config.configContainer) ?? null, + user: toNullableNumber(config.user), + workingDirectory: config.workingDirectory ?? null, + tty: toNullableBoolean(config.tty), ports: toPrismaJson(config.ports), portRanges: toPrismaJson(config.portRanges), volumes: toPrismaJson(config.volumes), @@ -86,23 +197,23 @@ export default class ContainerMapper { secrets: toPrismaJson(config.secrets), initContainers: toPrismaJson(config.initContainers), logConfig: toPrismaJson(config.logConfig), - storageSet: config.storageSet ?? undefined, - storageId: config.storageId ?? undefined, + storageSet: toNullableBoolean(config.storageSet), + storageId: config.storageId ?? null, storageConfig: toPrismaJson(config.storageConfig), // dagent - restartPolicy: config.restartPolicy ?? undefined, - networkMode: config.networkMode ?? undefined, + restartPolicy: config.restartPolicy ?? null, + networkMode: config.networkMode ?? null, networks: toPrismaJson(config.networks), dockerLabels: toPrismaJson(config.dockerLabels), expectedState: toPrismaJson(config.expectedState), // crane - deploymentStrategy: config.deploymentStrategy ?? undefined, + deploymentStrategy: config.deploymentStrategy ?? null, healthCheckConfig: toPrismaJson(config.healthCheckConfig), resourceConfig: toPrismaJson(config.resourceConfig), - proxyHeaders: config.proxyHeaders !== null ? config.proxyHeaders : false, - useLoadBalancer: config.useLoadBalancer !== null ? config.useLoadBalancer : false, + proxyHeaders: toNullableBoolean(config.proxyHeaders), + useLoadBalancer: toNullableBoolean(config.useLoadBalancer), customHeaders: toPrismaJson(config.customHeaders), extraLBAnnotations: toPrismaJson(config.extraLBAnnotations), capabilities: toPrismaJson(config.capabilities), @@ -112,94 +223,136 @@ export default class ContainerMapper { } } - mergeSecrets(instanceSecrets: UniqueSecretKeyValue[], imageSecrets: UniqueSecretKey[]): UniqueSecretKeyValue[] { - imageSecrets = imageSecrets ?? [] - instanceSecrets = instanceSecrets ?? [] - - const overriddenIds: Set = new Set(instanceSecrets?.map(it => it.id)) + configDataToDbPatch(config: ContainerConfigData): ContainerConfigDbPatch { + return { + name: 'name' in config ? config.name ?? null : undefined, + expose: 'expose' in config ? config.expose ?? null : undefined, + routing: 'routing' in config ? toPrismaJson(config.routing) : undefined, + configContainer: 'configContainer' in config ? toPrismaJson(config.configContainer) : undefined, + user: 'user' in config ? toNullableNumber(config.user) : undefined, + workingDirectory: 'workingDirectory' in config ? config.workingDirectory ?? null : undefined, + tty: 'tty' in config ? toNullableBoolean(config.tty) : undefined, + ports: 'ports' in config ? toPrismaJson(config.ports) : undefined, + portRanges: 'portRanges' in config ? toPrismaJson(config.portRanges) : undefined, + volumes: 'volumes' in config ? toPrismaJson(config.volumes) : undefined, + commands: 'commands' in config ? toPrismaJson(config.commands) : undefined, + args: 'args' in config ? toPrismaJson(config.args) : undefined, + environment: 'environment' in config ? toPrismaJson(config.environment) : undefined, + secrets: 'secrets' in config ? toPrismaJson(config.secrets) : undefined, + initContainers: 'initContainers' in config ? toPrismaJson(config.initContainers) : undefined, + logConfig: 'logConfig' in config ? toPrismaJson(config.logConfig) : undefined, + storageSet: 'storageSet' in config ? toNullableBoolean(config.storageSet) : undefined, + storageId: 'storageId' in config ? config.storageId ?? null : undefined, + storageConfig: 'storageConfig' in config ? toPrismaJson(config.storageConfig) : undefined, - const missing: UniqueSecretKeyValue[] = imageSecrets - .filter(it => !overriddenIds.has(it.id)) - .map(it => ({ - ...it, - value: '', - encrypted: false, - publicKey: null, - })) + // dagent + restartPolicy: 'restartPolicy' in config ? config.restartPolicy ?? null : undefined, + networkMode: 'networkMode' in config ? config.networkMode ?? null : undefined, + networks: 'networks' in config ? toPrismaJson(config.networks) : undefined, + dockerLabels: 'dockerLabels' in config ? toPrismaJson(config.dockerLabels) : undefined, + expectedState: 'expectedState' in config ? toPrismaJson(config.expectedState) : undefined, - return [...missing, ...instanceSecrets] + // crane + deploymentStrategy: 'deploymentStrategy' in config ? config.deploymentStrategy ?? null : undefined, + healthCheckConfig: 'healthCheckConfig' in config ? toPrismaJson(config.healthCheckConfig) : undefined, + resourceConfig: 'resourceConfig' in config ? toPrismaJson(config.resourceConfig) : undefined, + proxyHeaders: 'proxyHeaders' in config ? toNullableBoolean(config.proxyHeaders) : undefined, + useLoadBalancer: 'useLoadBalancer' in config ? toNullableBoolean(config.useLoadBalancer) : undefined, + customHeaders: 'customHeaders' in config ? toPrismaJson(config.customHeaders) : undefined, + extraLBAnnotations: 'extraLBAnnotations' in config ? toPrismaJson(config.extraLBAnnotations) : undefined, + capabilities: 'capabilities' in config ? toPrismaJson(config.capabilities) : undefined, + annotations: 'annotations' in config ? toPrismaJson(config.annotations) : undefined, + labels: 'labels' in config ? toPrismaJson(config.labels) : undefined, + metrics: 'metrics' in config ? toPrismaJson(config.metrics) : undefined, + } } - mergeMetrics(instance: Metrics, image: Metrics): Metrics { - if (!instance) { - return image?.enabled ? image : null + configUpdatedEventToMessage(event: ContainerConfigUpdatedEvent): ConfigUpdatedMessage { + return { + ...event.patch, + id: event.id, } - - return instance } - mergeConfigs(image: ContainerConfigData, instance: InstanceContainerConfigData): MergedContainerConfigData { - return { - // common - name: instance.name ?? image.name, - environment: instance.environment ?? image.environment, - secrets: this.mergeSecrets(instance.secrets, image.secrets), - user: instance.user ?? image.user, - workingDirectory: instance.workingDirectory ?? image.workingDirectory, - tty: instance.tty ?? image.tty, - portRanges: instance.portRanges ?? image.portRanges, - args: instance.args ?? image.args, - commands: instance.commands ?? image.commands, - expose: instance.expose ?? image.expose, - configContainer: instance.configContainer ?? image.configContainer, - routing: instance.routing ?? image.routing, - volumes: instance.volumes ?? image.volumes, - initContainers: instance.initContainers ?? image.initContainers, - capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing - ports: instance.ports ?? image.ports, - storageSet: instance.storageSet || image.storageSet, - storageId: instance.storageSet ? instance.storageId : image.storageId, - storageConfig: instance.storageSet ? instance.storageConfig : image.storageConfig, + private configDetailsToParentDto(config: ContainerConfigDetails): ContainerConfigParentDto { + switch (config.type) { + case 'image': { + const { image } = config - // crane - customHeaders: instance.customHeaders ?? image.customHeaders, - proxyHeaders: instance.proxyHeaders ?? image.proxyHeaders, - extraLBAnnotations: instance.extraLBAnnotations ?? image.extraLBAnnotations, - healthCheckConfig: instance.healthCheckConfig ?? image.healthCheckConfig, - resourceConfig: instance.resourceConfig ?? image.resourceConfig, - useLoadBalancer: instance.useLoadBalancer ?? image.useLoadBalancer, - deploymentStrategy: instance.deploymentStrategy ?? image.deploymentStrategy, - labels: - instance.labels || image.labels - ? { - deployment: instance.labels?.deployment ?? image.labels?.deployment ?? [], - service: instance.labels?.service ?? image.labels?.service ?? [], - ingress: instance.labels?.ingress ?? image.labels?.ingress ?? [], - } - : null, - annotations: - image.annotations || instance.annotations - ? { - deployment: instance.annotations?.deployment ?? image.annotations?.deployment ?? [], - service: instance.annotations?.service ?? image.annotations?.service ?? [], - ingress: instance.annotations?.ingress ?? image.annotations?.ingress ?? [], - } - : null, - metrics: this.mergeMetrics(instance.metrics, image.metrics), + return { + id: image.id, + name: image.name, + mutable: versionIsMutable(image.version), + } + } + case 'instance': { + const { instance } = config + const { image, deployment } = instance - // dagent - logConfig: instance.logConfig ?? image.logConfig, - networkMode: instance.networkMode ?? image.networkMode, - restartPolicy: instance.restartPolicy ?? image.restartPolicy, - networks: instance.networks ?? image.networks, - dockerLabels: instance.dockerLabels ?? image.dockerLabels, - expectedState: - !!image.expectedState || !!instance.expectedState - ? { - ...image.expectedState, - ...instance.expectedState, - } - : null, + return { + id: image.id, + name: image.name, + mutable: deploymentIsMutable(deployment.status, deployment.version.type), + } + } + case 'deployment': { + const { deployment } = config + + return { + id: deployment.id, + name: deployment.prefix, + mutable: deploymentIsMutable(deployment.status, deployment.version.type), + } + } + case 'configBundle': { + const { configBundle } = config + + return { + id: configBundle.id, + name: configBundle.name, + mutable: true, + } + } + default: + throw new Error(`Unknown ContainerConfigType ${config.type}`) + } + } +} + +type ContainerConfigRelations = { + type: ContainerConfigType + image: ImageDetails & { + version: Version & { + project: Project + } + } + instance: { + image: ImageDetails + deployment: DeploymentWithConfigAndBundles + } + deployment: DeploymentWithConfigAndBundles + configBundle: ConfigBundle +} + +type ContainerConfigDetails = ContainerConfig & { + image: Image & { + version: Version & { + deployments: { + status: DeploymentStatusEnum + }[] + children: VersionsOnParentVersion[] } } + instance: { + image: Image + deployment: Deployment & { + version: Version + } + } + deployment: Deployment & { + version: Version + } + configBundle: ConfigBundle } + +export type ContainerConfigDbPatch = Omit diff --git a/web/crux/src/app/container/container.module.ts b/web/crux/src/app/container/container.module.ts index 6a2bb27452..a98e17da1e 100644 --- a/web/crux/src/app/container/container.module.ts +++ b/web/crux/src/app/container/container.module.ts @@ -1,10 +1,38 @@ -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' +import PrismaService from 'src/services/prisma.service' +import AgentModule from '../agent/agent.module' +import AuditLoggerModule from '../audit.logger/audit.logger.module' +import ConfigBundleModule from '../config.bundle/config.bundle.module' +import DeployModule from '../deploy/deploy.module' +import EditorModule from '../editor/editor.module' +import ImageModule from '../image/image.module' +import ProjectModule from '../project/project.module' +import VersionModule from '../version/version.module' +import ContainerConfigDomainEventListener from './container-config.domain-event.listener' +import ContainerConfigHttpController from './container-config.http.service' +import ContainerConfigService from './container-config.service' +import ContainerConfigWebSocketGateway from './container-config.ws.gateway' import ContainerMapper from './container.mapper' @Module({ - imports: [], - exports: [ContainerMapper], - controllers: [], - providers: [ContainerMapper], + imports: [ + AuditLoggerModule, + EditorModule, + forwardRef(() => AgentModule), + forwardRef(() => ProjectModule), + forwardRef(() => VersionModule), + forwardRef(() => ImageModule), + forwardRef(() => DeployModule), + forwardRef(() => ConfigBundleModule), + ], + exports: [ContainerMapper, ContainerConfigService], + controllers: [ContainerConfigHttpController], + providers: [ + PrismaService, + ContainerMapper, + ContainerConfigService, + ContainerConfigDomainEventListener, + ContainerConfigWebSocketGateway, + ], }) export default class ContainerModule {} diff --git a/web/crux/src/app/container/guards/container-config.team-access.guard.ts b/web/crux/src/app/container/guards/container-config.team-access.guard.ts new file mode 100644 index 0000000000..412122843c --- /dev/null +++ b/web/crux/src/app/container/guards/container-config.team-access.guard.ts @@ -0,0 +1,22 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' +import { AuthorizedHttpRequest, identityOfRequest } from 'src/app/token/jwt-auth.guard' +import ContainerConfigService from '../container-config.service' + +@Injectable() +export default class ContainerConfigTeamAccessGuard implements CanActivate { + constructor(private readonly service: ContainerConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest() as AuthorizedHttpRequest + const teamSlug = req.params.teamSlug as string + const configId = req.params.configId as string + + if (!configId) { + return true + } + + const identity = identityOfRequest(context) + + return await this.service.checkConfigIsInTeam(teamSlug, configId, identity) + } +} diff --git a/web/crux/src/app/dashboard/dashboard.mapper.spec.ts b/web/crux/src/app/dashboard/dashboard.mapper.spec.ts index 7cf7fc6bbc..3b86549a7a 100644 --- a/web/crux/src/app/dashboard/dashboard.mapper.spec.ts +++ b/web/crux/src/app/dashboard/dashboard.mapper.spec.ts @@ -21,7 +21,9 @@ describe('DashboardMapper', () => { nodes: [], project: null, version: null, - image: null, + image: { + config: null, + }, deployment: null, } }) @@ -140,9 +142,9 @@ describe('DashboardMapper', () => { }) test('should be done when there is an image', () => { - const IMAGE_ID = 'imageId' + const IMAGE_CONFIG_ID = 'imageConfigId' - const expected: OnboardingItemDto = { done: true, resourceId: IMAGE_ID } + const expected: OnboardingItemDto = { done: true, resourceId: IMAGE_CONFIG_ID } team.project = { id: 'projectId', @@ -151,7 +153,9 @@ describe('DashboardMapper', () => { id: 'versionid', } team.image = { - id: IMAGE_ID, + config: { + id: IMAGE_CONFIG_ID, + }, } const actual = mapper.teamToOnboard(team) @@ -186,7 +190,9 @@ describe('DashboardMapper', () => { id: 'versionid', } team.image = { - id: 'imageId', + config: { + id: 'imageConfigId', + }, } team.deployment = { id: DEPLOYMENT_ID, @@ -226,7 +232,9 @@ describe('DashboardMapper', () => { id: 'versionid', } team.image = { - id: 'imageId', + config: { + id: 'imageConfigId', + }, } team.deployment = { id: DEPLOYMENT_ID, @@ -253,7 +261,9 @@ describe('DashboardMapper', () => { id: 'versionid', } team.image = { - id: 'imageId', + config: { + id: 'imageConfigId', + }, } team.deployment = { id: DEPLOYMENT_ID, diff --git a/web/crux/src/app/dashboard/dashboard.mapper.ts b/web/crux/src/app/dashboard/dashboard.mapper.ts index 5f0165a6bd..2963198c54 100644 --- a/web/crux/src/app/dashboard/dashboard.mapper.ts +++ b/web/crux/src/app/dashboard/dashboard.mapper.ts @@ -18,7 +18,7 @@ export default class DashboardMapper { createNode: this.resourceToOnboardItem(deployment ? deployment.node : team.nodes.find(Boolean)), createProject: this.resourceToOnboardItem(team.project), createVersion: this.resourceToOnboardItem(team.version), - addImages: this.resourceToOnboardItem(team.image), + addImages: this.resourceToOnboardItem(team.image.config), addDeployment: this.resourceToOnboardItem(deployment), deploy: { done: (deployment && deployment.status !== 'preparing') ?? false, @@ -55,6 +55,8 @@ export type DashboardTeam = ResourceWithId & { nodes: ResourceWithId[] project: ResourceWithId version: ResourceWithId - image: ResourceWithId + image: { + config: ResourceWithId + } deployment: DashboardDeployment } diff --git a/web/crux/src/app/dashboard/dashboard.service.ts b/web/crux/src/app/dashboard/dashboard.service.ts index e1a16617ab..e74420f689 100644 --- a/web/crux/src/app/dashboard/dashboard.service.ts +++ b/web/crux/src/app/dashboard/dashboard.service.ts @@ -42,7 +42,9 @@ export default class DashboardService { ...team, project: null, version: null, - image: null, + image: { + config: null, + }, deployment: null, } @@ -131,6 +133,7 @@ export default class DashboardService { images: { select: { id: true, + config: true, }, take: 1, orderBy: { @@ -151,18 +154,13 @@ export default class DashboardService { return null } - const { - version, - version: { - images: [image], - project, - }, - } = deployment + const { version } = deployment + const { project, images } = version return { project, version, - image, + image: images.find(Boolean), deployment, } } @@ -190,7 +188,11 @@ export default class DashboardService { id: true, images: { select: { - id: true, + config: { + select: { + id: true, + }, + }, }, take: 1, orderBy: { diff --git a/web/crux/src/app/deploy/deploy.domain-event.listener.ts b/web/crux/src/app/deploy/deploy.domain-event.listener.ts new file mode 100644 index 0000000000..c7d657cb2b --- /dev/null +++ b/web/crux/src/app/deploy/deploy.domain-event.listener.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@nestjs/common' +import { OnEvent } from '@nestjs/event-emitter' +import { filter, Observable, Subject } from 'rxjs' +import { + DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE, + DEPLOYMENT_EVENT_INSTACE_CREATE, + DEPLOYMENT_EVENT_INSTACE_DELETE, + DeploymentConfigBundlesUpdatedEvent, + DeploymentEditEvent, + IMAGE_EVENT_ADD, + IMAGE_EVENT_DELETE, + ImageDeletedEvent, + ImagesAddedEvent, + InstanceDeletedEvent, + InstancesCreatedEvent, +} from 'src/domain/domain-events' +import PrismaService from 'src/services/prisma.service' +import { DomainEvent } from 'src/shared/domain-event' + +@Injectable() +export default class DeployDomainEventListener { + private deploymentEvents = new Subject>() + + constructor(private prisma: PrismaService) {} + + watchEvents(deploymentId: string): Observable> { + return this.deploymentEvents.pipe(filter(it => it.event.deploymentId === deploymentId)) + } + + @OnEvent(IMAGE_EVENT_ADD, { async: true }) + async onImagesAdded(event: ImagesAddedEvent) { + const deployments = await this.prisma.deployment.findMany({ + select: { + id: true, + }, + where: { + versionId: event.versionId, + }, + }) + + const createEvents: InstancesCreatedEvent[] = await Promise.all( + deployments.map(async deployment => { + const instances = await Promise.all( + event.images.map(it => + this.prisma.instance.create({ + select: { + configId: true, + id: true, + image: { + include: { + config: true, + registry: true, + }, + }, + }, + data: { + deployment: { connect: { id: deployment.id } }, + image: { connect: { id: it.id } }, + config: { create: { type: 'instance' } }, + }, + }), + ), + ) + + return { + deploymentId: deployment.id, + instances, + } + }), + ) + + this.sendEditEvents(DEPLOYMENT_EVENT_INSTACE_CREATE, createEvents) + } + + @OnEvent(IMAGE_EVENT_DELETE) + onImageDeleted(event: ImageDeletedEvent) { + const deleteEvents: InstanceDeletedEvent[] = event.instances + + this.sendEditEvents(DEPLOYMENT_EVENT_INSTACE_DELETE, deleteEvents) + } + + @OnEvent(DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE) + onConfigBundlesUpdated(event: DeploymentConfigBundlesUpdatedEvent) { + const editEvent: DomainEvent = { + type: DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE, + event, + } + + this.deploymentEvents.next(editEvent) + } + + private sendEditEvents(type: string, events: DeploymentEditEvent[]) { + events.forEach(it => + this.deploymentEvents.next({ + type, + event: it, + }), + ) + } +} diff --git a/web/crux/src/app/deploy/deploy.dto.ts b/web/crux/src/app/deploy/deploy.dto.ts index 217e7b49d5..d714ce0d51 100644 --- a/web/crux/src/app/deploy/deploy.dto.ts +++ b/web/crux/src/app/deploy/deploy.dto.ts @@ -1,5 +1,4 @@ -import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger' -import { Deployment, DeploymentToken, Instance, InstanceContainerConfig, Node, Project, Version } from '@prisma/client' +import { ApiProperty } from '@nestjs/swagger' import { Type } from 'class-transformer' import { IsBoolean, @@ -8,7 +7,6 @@ import { IsInt, IsJWT, IsNumber, - IsObject, IsOptional, IsString, IsUUID, @@ -18,26 +16,48 @@ import { } from 'class-validator' import { CONTAINER_STATE_VALUES, ContainerState } from 'src/domain/container' import { PaginatedList, PaginationQuery } from 'src/shared/dtos/paginating' -import { BasicProperties } from '../../shared/dtos/shared.dto' import { AuditDto } from '../audit/audit.dto' +import { ConfigBundleDetailsDto } from '../config.bundle/config.bundle.dto' import { - ContainerConfigDto, + ConcreteContainerConfigDataDto, + ConcreteContainerConfigDto, ContainerIdentifierDto, - UniqueKeyValueDto, - UniqueSecretKeyValueDto, } from '../container/container.dto' -import { ImageDto } from '../image/image.dto' -import { ImageEvent } from '../image/image.event' -import { ImageDetails } from '../image/image.mapper' +import { ImageDetailsDto } from '../image/image.dto' import { BasicNodeDto, BasicNodeWithStatus } from '../node/node.dto' import { BasicProjectDto } from '../project/project.dto' import { BasicVersionDto } from '../version/version.dto' -const DEPLOYMENT_STATUS_VALUES = ['preparing', 'in-progress', 'successful', 'failed', 'obsolete'] as const +export const DEPLOYMENT_STATUS_VALUES = ['preparing', 'in-progress', 'successful', 'failed', 'obsolete'] as const export type DeploymentStatusDto = (typeof DEPLOYMENT_STATUS_VALUES)[number] export type EnvironmentToConfigBundleNameMap = Record +export class DeploymentQueryDto extends PaginationQuery { + @IsOptional() + @IsString() + @Type(() => String) + @ApiProperty() + readonly nodeId?: string + + @IsOptional() + @IsString() + @Type(() => String) + @ApiProperty() + readonly filter?: string + + @IsOptional() + @ApiProperty({ enum: DEPLOYMENT_STATUS_VALUES }) + @IsIn(DEPLOYMENT_STATUS_VALUES) + readonly status?: DeploymentStatusDto + + @IsOptional() + @IsString() + @Type(() => String) + @ApiProperty() + readonly configBundleId?: string +} + export class BasicDeploymentDto { @IsUUID() id: string @@ -84,12 +104,6 @@ export class DeploymentWithBasicNodeDto extends BasicDeploymentDto { node: BasicNodeWithStatus } -export class InstanceContainerConfigDto extends OmitType(PartialType(ContainerConfigDto), ['secrets']) { - @IsOptional() - @ValidateNested({ each: true }) - secrets?: UniqueSecretKeyValueDto[] -} - export class InstanceDto { @IsUUID() id: string @@ -99,11 +113,18 @@ export class InstanceDto { updatedAt: Date @ValidateNested() - image: ImageDto + image: ImageDetailsDto +} + +export class InstanceDetailsDto extends InstanceDto { + @ValidateNested() + config: ConcreteContainerConfigDto +} +export class PatchInstanceDto { @IsOptional() @ValidateNested() - config?: InstanceContainerConfigDto | null + config?: ConcreteContainerConfigDataDto | null } export class DeploymentTokenDto { @@ -142,30 +163,30 @@ export class DeploymentTokenCreatedDto extends DeploymentTokenDto { curl: string } -export class DeploymentDetailsDto extends DeploymentDto { - @ValidateNested({ each: true }) - environment: UniqueKeyValueDto[] - - @IsObject() - configBundleEnvironment: EnvironmentToConfigBundleNameMap - +export class DeploymentWithConfigDto extends DeploymentDto { @IsString() @IsOptional() publicKey?: string | null @ValidateNested() - instances: InstanceDto[] + config: ConcreteContainerConfigDto + + @IsString({ each: true }) + @IsOptional() + configBundles: ConfigBundleDetailsDto[] +} + +export class DeploymentDetailsDto extends DeploymentWithConfigDto { + @ValidateNested({ each: true }) + instances: InstanceDetailsDto[] @IsInt() @Min(0) lastTry: number @ValidateNested() - token: DeploymentTokenDto - - @IsString({ each: true }) @IsOptional() - configBundleIds: string[] + token?: DeploymentTokenDto } export class CreateDeploymentDto { @@ -186,31 +207,19 @@ export class CreateDeploymentDto { note?: string | null } -export class PatchDeploymentDto { +export class UpdateDeploymentDto { @IsString() @IsOptional() note?: string | null @IsString() - @IsOptional() - prefix?: string | null + prefix: string @IsBoolean() - @IsOptional() - protected?: boolean - - @IsOptional() - @ValidateNested({ each: true }) - environment?: UniqueKeyValueDto[] | null + protected: boolean @IsString({ each: true }) - @IsOptional() - configBundleIds?: string[] -} - -export class PatchInstanceDto { - @ValidateNested() - config: InstanceContainerConfigDto + configBundles: string[] } export class CopyDeploymentDto { @@ -290,16 +299,17 @@ export class DeploymentEventDto { containerProgress?: DeploymentEventContainerProgressDto | null } -export class InstanceSecretsDto { - @ValidateNested() - container: ContainerIdentifierDto - +export class DeploymentSecretsDto { @IsString() publicKey: string - @IsOptional() @IsString({ each: true }) - keys?: string[] | null + keys: string[] +} + +export class InstanceSecretsDto extends DeploymentSecretsDto { + @ValidateNested() + container: ContainerIdentifierDto } export class DeploymentLogListDto extends PaginatedList { @@ -324,32 +334,9 @@ export class StartDeploymentDto { instances?: string[] } -export type DeploymentImageEvent = ImageEvent & { - deploymentIds?: string[] - instances?: InstanceDetails[] -} - -export type DeploymentWithNode = Deployment & { - node: Pick -} - -export type DeploymentWithNodeVersion = DeploymentWithNode & { - version: Pick & { - project: Pick - } -} - -export type InstanceDetails = Instance & { - image: ImageDetails - config?: InstanceContainerConfig -} +export class DeploymentListDto extends PaginatedList { + @Type(() => DeploymentDto) + items: DeploymentDto[] -export type DeploymentDetails = DeploymentWithNodeVersion & { - tokens: Pick[] - instances: InstanceDetails[] - configBundles: { - configBundle: { - id: string - } - }[] + total: number } diff --git a/web/crux/src/app/deploy/deploy.http.controller.ts b/web/crux/src/app/deploy/deploy.http.controller.ts index 5bd22a191b..0662a45990 100644 --- a/web/crux/src/app/deploy/deploy.http.controller.ts +++ b/web/crux/src/app/deploy/deploy.http.controller.ts @@ -35,14 +35,17 @@ import { CreateDeploymentTokenDto, DeploymentDetailsDto, DeploymentDto, + DeploymentListDto, DeploymentLogListDto, DeploymentLogPaginationQuery, + DeploymentQueryDto, + DeploymentSecretsDto, DeploymentTokenCreatedDto, - InstanceDto, + InstanceDetailsDto, InstanceSecretsDto, - PatchDeploymentDto, PatchInstanceDto, StartDeploymentDto, + UpdateDeploymentDto, } from './deploy.dto' import DeployService from './deploy.service' import DeployCreateTeamAccessGuard from './guards/deploy.create.team-access.guard' @@ -65,6 +68,7 @@ const InstanceId = () => Param(PARAM_INSTANCE_ID) const ROUTE_TEAM_SLUG = ':teamSlug' const ROUTE_DEPLOYMENTS = 'deployments' const ROUTE_DEPLOYMENT_ID = ':deploymentId' +const ROUTE_SECRETS = 'secrets' const ROUTE_INSTANCES = 'instances' const ROUTE_INSTANCE_ID = ':instanceId' const ROUTE_TOKEN = 'token' @@ -79,17 +83,13 @@ export default class DeployHttpController { @HttpCode(HttpStatus.OK) @ApiOperation({ description: - 'Get the list of deployments. Request needs to include `teamSlug` in URL. A deployment should include `id`, `prefix`, `status`, `note`, `audit` log details, project `name`, `id`, `type`, version `name`, `type`, `id`, and node `name`, `id`, `type`.', + 'Get the list of deployments. Request needs to include `teamSlug` in URL. Query could include `skip` and `take` to paginate. A deployment should include `id`, `prefix`, `status`, `note`, `audit` log details, project `name`, `id`, `type`, version `name`, `type`, `id`, and node `name`, `id`, `type`.', summary: 'Fetch the list of deployments.', }) - @ApiOkResponse({ - type: DeploymentDto, - isArray: true, - description: 'List of deployments.', - }) + @ApiOkResponse({ type: DeploymentQueryDto, description: 'Paginated list of deployments.' }) @ApiForbiddenResponse({ description: 'Unauthorized request for deployments.' }) - async getDeployments(@TeamSlug() teamSlug: string): Promise { - return await this.service.getDeployments(teamSlug) + async getDeployments(@TeamSlug() teamSlug: string, @Query() query: DeploymentQueryDto): Promise { + return await this.service.getDeployments(teamSlug, query) } @Get(ROUTE_DEPLOYMENT_ID) @@ -111,6 +111,25 @@ export default class DeployHttpController { return await this.service.getDeploymentDetails(deploymentId) } + @Get(`${ROUTE_DEPLOYMENT_ID}/${ROUTE_SECRETS}`) + @HttpCode(HttpStatus.OK) + @ApiOperation({ + description: + 'Request must include `teamSlug` and `deploymentId`, which refers to the ID of a deployment, needs to be included in URL. Response should include `publicKey`, `keys`.', + summary: 'Fetch secrets of a deployment.', + }) + @ApiOkResponse({ type: DeploymentSecretsDto, description: 'Secrets of a deployment listed.' }) + @ApiBadRequestResponse({ description: 'Bad request for deployment secrets.' }) + @ApiForbiddenResponse({ description: 'Unauthorized request for deployment secrets.' }) + @ApiNotFoundResponse({ description: 'Deployment secrets not found.' }) + @UuidParams(PARAM_DEPLOYMENT_ID) + async getDeploymentSecrets( + @TeamSlug() _: string, + @DeploymentId() deploymentId: string, + ): Promise { + return await this.service.getDeploymentSecrets(deploymentId) + } + @Get(`${ROUTE_DEPLOYMENT_ID}/${ROUTE_INSTANCES}/${ROUTE_INSTANCE_ID}`) @HttpCode(HttpStatus.OK) @ApiOperation({ @@ -118,7 +137,7 @@ export default class DeployHttpController { 'Request must include `teamSlug`, `deploymentId` and `instanceId`, which refer to the ID of a deployment and the instance, in the URL. Instances are the manifestation of an image in the deployment. Response should include `state`, `id`, `updatedAt`, and `image` details including `id`, `name`, `tag`, `order` and `config` variables.', summary: 'Get details of a soon-to-be container.', }) - @ApiOkResponse({ type: InstanceDto, description: 'Details of an instance.' }) + @ApiOkResponse({ type: InstanceDetailsDto, description: 'Details of an instance.' }) @ApiBadRequestResponse({ description: 'Bad request for instance details.' }) @ApiForbiddenResponse({ description: 'Unauthorized request for an instance.' }) @ApiNotFoundResponse({ description: 'Instance not found.' }) @@ -127,11 +146,11 @@ export default class DeployHttpController { @TeamSlug() _: string, @DeploymentId() _deploymentId: string, @InstanceId() instanceId: string, - ): Promise { + ): Promise { return await this.service.getInstance(instanceId) } - @Get(`${ROUTE_DEPLOYMENT_ID}/${ROUTE_INSTANCES}/${ROUTE_INSTANCE_ID}/secrets`) + @Get(`${ROUTE_DEPLOYMENT_ID}/${ROUTE_INSTANCES}/${ROUTE_INSTANCE_ID}/${ROUTE_SECRETS}`) @HttpCode(HttpStatus.OK) @ApiOperation({ description: @@ -143,7 +162,7 @@ export default class DeployHttpController { @ApiForbiddenResponse({ description: 'Unauthorized request for instance secrets.' }) @ApiNotFoundResponse({ description: 'Instance secrets not found.' }) @UuidParams(PARAM_DEPLOYMENT_ID, PARAM_INSTANCE_ID) - async getDeploymentSecrets( + async getInstanceSecrets( @TeamSlug() _: string, @DeploymentId() _deploymentId: string, @InstanceId() instanceId: string, @@ -179,7 +198,7 @@ export default class DeployHttpController { } } - @Patch(ROUTE_DEPLOYMENT_ID) + @Put(ROUTE_DEPLOYMENT_ID) @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ description: 'Request must include `deploymentId` and `teamSlug` in URL.', @@ -194,10 +213,10 @@ export default class DeployHttpController { async patchDeployment( @TeamSlug() _: string, @DeploymentId() deploymentId: string, - @Body() request: PatchDeploymentDto, + @Body() request: UpdateDeploymentDto, @IdentityFromRequest() identity: Identity, ): Promise { - await this.service.patchDeployment(deploymentId, request, identity) + await this.service.updateDeployment(deploymentId, request, identity) } @Patch(`${ROUTE_DEPLOYMENT_ID}/${ROUTE_INSTANCES}/${ROUTE_INSTANCE_ID}`) diff --git a/web/crux/src/app/deploy/deploy.mapper.spec.ts b/web/crux/src/app/deploy/deploy.mapper.spec.ts index c2daf48fc3..6fc9e00c38 100644 --- a/web/crux/src/app/deploy/deploy.mapper.spec.ts +++ b/web/crux/src/app/deploy/deploy.mapper.spec.ts @@ -1,22 +1,23 @@ import { DeploymentStatusEnum, NodeTypeEnum, ProjectTypeEnum, Storage, VersionTypeEnum } from '.prisma/client' import { Test, TestingModule } from '@nestjs/testing' -import { ContainerConfigData, InstanceContainerConfigData, MergedContainerConfigData } from 'src/domain/container' +import { ConcreteContainerConfigData, ContainerConfigData } from 'src/domain/container' +import { DeploymentWithNodeVersion } from 'src/domain/deployment' import { CommonContainerConfig, DagentContainerConfig, ImportContainer } from 'src/grpc/protobuf/proto/agent' import { DriverType, NetworkMode, RestartPolicy } from 'src/grpc/protobuf/proto/common' import EncryptionService from 'src/services/encryption.service' import AgentService from '../agent/agent.service' import AuditMapper from '../audit/audit.mapper' +import ConfigBundleMapper from '../config.bundle/config.bundle.mapper' import ContainerMapper from '../container/container.mapper' import ImageMapper from '../image/image.mapper' import NodeMapper from '../node/node.mapper' import ProjectMapper from '../project/project.mapper' import RegistryMapper from '../registry/registry.mapper' import VersionMapper from '../version/version.mapper' -import { DeploymentDto, DeploymentWithNodeVersion, PatchInstanceDto } from './deploy.dto' +import { DeploymentDto, PatchInstanceDto } from './deploy.dto' import DeployMapper from './deploy.mapper' describe('DeployMapper', () => { - let containerMapper: ContainerMapper = null let deployMapper: DeployMapper = null beforeEach(async () => { @@ -32,6 +33,7 @@ describe('DeployMapper', () => { NodeMapper, ImageMapper, DeployMapper, + ConfigBundleMapper, { provide: EncryptionService, useValue: jest.mocked(EncryptionService), @@ -43,7 +45,6 @@ describe('DeployMapper', () => { ], }).compile() - containerMapper = module.get(ContainerMapper) deployMapper = module.get(DeployMapper) }) @@ -268,7 +269,7 @@ describe('DeployMapper', () => { expectedState: null, } - const fullInstance: InstanceContainerConfigData = { + const fullInstance: ConcreteContainerConfigData = { name: 'instance.img', capabilities: [], deploymentStrategy: 'recreate', @@ -492,8 +493,8 @@ describe('DeployMapper', () => { expectedState: null, } - const generateUndefinedInstance = (): InstanceContainerConfigData => { - const instance: InstanceContainerConfigData = {} + const generateUndefinedInstance = (): ConcreteContainerConfigData => { + const instance: ConcreteContainerConfigData = {} Object.keys(fullImage).forEach(key => { instance[key] = undefined }) @@ -501,85 +502,6 @@ describe('DeployMapper', () => { return instance } - describe('mergeConfigs', () => { - it('should use the instance variables when available', () => { - const merged = containerMapper.mergeConfigs(fullImage, fullInstance) - - expect(merged).toEqual(fullInstance) - }) - - it('should use the image variables when instance is not available', () => { - const merged = containerMapper.mergeConfigs(fullImage, {}) - - const expected: InstanceContainerConfigData = { - ...fullImage, - secrets: [ - { - id: 'secret1', - key: 'secret1', - required: false, - encrypted: false, - value: '', - publicKey: null, - }, - ], - } - - expect(merged).toEqual(expected) - }) - - it('should use the instance only when available', () => { - const instance: InstanceContainerConfigData = { - ports: fullInstance.ports, - labels: { - deployment: [ - { - id: 'instance.labels.deployment', - key: 'instance.labels.deployment', - value: 'instance.labels.deployment', - }, - ], - }, - annotations: { - service: [ - { - id: 'instance.annotations.service', - key: 'instance.annotations.service', - value: 'instance.annotations.service', - }, - ], - }, - } - - const expected: InstanceContainerConfigData = { - ...fullImage, - ports: fullInstance.ports, - labels: { - ...fullImage.labels, - deployment: instance.labels.deployment, - }, - annotations: { - ...fullImage.annotations, - service: instance.annotations.service, - }, - secrets: [ - { - id: 'secret1', - key: 'secret1', - required: false, - encrypted: false, - value: '', - publicKey: null, - }, - ], - } - - const merged = containerMapper.mergeConfigs(fullImage, instance) - - expect(merged).toEqual(expected) - }) - }) - describe('instanceConfigToInstanceContainerConfigData', () => { it('should overwrite the specified properties only', () => { const patch: PatchInstanceDto = { @@ -606,7 +528,7 @@ describe('DeployMapper', () => { }, ] - const actual = deployMapper.instanceConfigDtoToInstanceContainerConfigData(fullImage, {}, patch.config) + const actual = deployMapper.concreteConfigDtoToConcreteContainerConfigData(fullImage, {}, patch.config) expect(actual).toEqual(expected) }) @@ -639,7 +561,7 @@ describe('DeployMapper', () => { deployment: fullInstance.labels.deployment, } - const actual = deployMapper.instanceConfigDtoToInstanceContainerConfigData(fullImage, {}, patch.config) + const actual = deployMapper.concreteConfigDtoToConcreteContainerConfigData(fullImage, {}, patch.config) expect(actual).toEqual(expected) }) @@ -676,7 +598,7 @@ describe('DeployMapper', () => { }, } - const instance: InstanceContainerConfigData = { + const instance: ConcreteContainerConfigData = { labels: fullInstance.labels, annotations: fullInstance.annotations, } @@ -693,7 +615,7 @@ describe('DeployMapper', () => { ingress: labelIngress, } - const actual = deployMapper.instanceConfigDtoToInstanceContainerConfigData(fullImage, instance, patch.config) + const actual = deployMapper.concreteConfigDtoToConcreteContainerConfigData(fullImage, instance, patch.config) expect(actual).toEqual(expected) }) @@ -710,6 +632,8 @@ describe('DeployMapper', () => { updatedAt: new Date(), createdBy: 'created-by', updatedBy: 'updated-by', + deployedAt: null, + deployedBy: null, node: { id: 'deployment-node-id', name: 'deployment node', @@ -725,7 +649,7 @@ describe('DeployMapper', () => { type: ProjectTypeEnum.versionless, }, }, - environment: {}, + configId: 'deployment-config-id', versionId: 'deployment-version-id', nodeId: 'deployment-node-id', tries: 1, @@ -768,7 +692,8 @@ describe('DeployMapper', () => { describe('commonConfigToAgentProto', () => { it('the function storageToImportContainer should add https by default if protocol is missing', () => { const config = deployMapper.commonConfigToAgentProto( - { + { + storageSet: true, storageId: 'test-1234', storageConfig: { path: 'test', @@ -791,7 +716,8 @@ describe('DeployMapper', () => { it('the function storageToImportContainer should leave http prefix untouched', () => { const config = deployMapper.commonConfigToAgentProto( - { + { + storageSet: true, storageId: 'test-1234', storageConfig: { path: 'test', @@ -814,7 +740,8 @@ describe('DeployMapper', () => { it('the function storageToImportContainer should add https prefix untouched', () => { const config = deployMapper.commonConfigToAgentProto( - { + { + storageSet: true, storageId: 'test-1234', storageConfig: { path: 'test', @@ -838,7 +765,7 @@ describe('DeployMapper', () => { describe('dagentConfigToAgentProto logConfig', () => { it('undefined logConfig should return no log driver', () => { - const config = deployMapper.dagentConfigToAgentProto({ + const config = deployMapper.dagentConfigToAgentProto({ networks: [], networkMode: 'host', restartPolicy: 'always', @@ -856,7 +783,7 @@ describe('DeployMapper', () => { }) it('node default driver type should return no log driver', () => { - const config = deployMapper.dagentConfigToAgentProto({ + const config = deployMapper.dagentConfigToAgentProto({ networks: [], networkMode: 'host', restartPolicy: 'always', @@ -877,7 +804,7 @@ describe('DeployMapper', () => { }) it('none driver type should return none log driver', () => { - const config = deployMapper.dagentConfigToAgentProto({ + const config = deployMapper.dagentConfigToAgentProto({ networks: [], networkMode: 'host', restartPolicy: 'always', diff --git a/web/crux/src/app/deploy/deploy.mapper.ts b/web/crux/src/app/deploy/deploy.mapper.ts index 7a658e886c..d32aabea7f 100644 --- a/web/crux/src/app/deploy/deploy.mapper.ts +++ b/web/crux/src/app/deploy/deploy.mapper.ts @@ -1,22 +1,41 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common' import { + ContainerConfig, Deployment, DeploymentEvent, DeploymentEventTypeEnum, DeploymentStatusEnum, - InstanceContainerConfig, + DeploymentStrategy, + ExposeStrategy, + NetworkMode, + RestartPolicy, Storage, } from '@prisma/client' import { + ConcreteContainerConfigData, ContainerConfigData, + ContainerLogDriverType, ContainerState, + ContainerVolumeType, InitContainer, - InstanceContainerConfigData, - MergedContainerConfigData, UniqueKey, - UniqueKeyValue, + Volume, } from 'src/domain/container' -import { deploymentLogLevelToDb, deploymentStatusToDb } from 'src/domain/deployment' +import { mergeMarkers, mergeSecrets } from 'src/domain/container-merge' +import { + DeploymentDetails, + DeploymentWithConfigAndBundles, + DeploymentWithNode, + DeploymentWithNodeVersion, + InstanceDetails, + deploymentLogLevelToDb, + deploymentStatusToDb, +} from 'src/domain/deployment' +import { + DeploymentConfigBundlesUpdatedEvent, + InstanceDeletedEvent, + InstancesCreatedEvent, +} from 'src/domain/domain-events' import { CruxInternalServerErrorException } from 'src/exception/crux-exception' import { InitContainer as AgentInitContainer, @@ -24,19 +43,29 @@ import { CraneContainerConfig, DagentContainerConfig, ImportContainer, - InstanceConfig, + Volume as ProtoVolume, } from 'src/grpc/protobuf/proto/agent' import { DeploymentStatusMessage, + DriverType, KeyValue, ListSecretsResponse, ContainerState as ProtoContainerState, DeploymentStrategy as ProtoDeploymentStrategy, ExposeStrategy as ProtoExposeStrategy, + NetworkMode as ProtoNetworkMode, + RestartPolicy as ProtoRestartPolicy, + VolumeType as ProtoVolumeType, containerStateToJSON, + driverTypeFromJSON, + networkModeFromJSON, + volumeTypeFromJSON, } from 'src/grpc/protobuf/proto/common' import EncryptionService from 'src/services/encryption.service' +import AgentService from '../agent/agent.service' import AuditMapper from '../audit/audit.mapper' +import ConfigBundleMapper from '../config.bundle/config.bundle.mapper' +import { ConcreteContainerConfigDataDto, ConcreteContainerConfigDto } from '../container/container.dto' import ContainerMapper from '../container/container.mapper' import ImageMapper from '../image/image.mapper' import { NodeConnectionStatus } from '../node/node.dto' @@ -45,39 +74,41 @@ import ProjectMapper from '../project/project.mapper' import VersionMapper from '../version/version.mapper' import { BasicDeploymentDto, - DeploymentDetails, DeploymentDetailsDto, DeploymentDto, DeploymentEventDto, DeploymentEventLogDto, DeploymentEventTypeDto, DeploymentLogLevelDto, + DeploymentSecretsDto, DeploymentStatusDto, DeploymentWithBasicNodeDto, - DeploymentWithNode, - DeploymentWithNodeVersion, - EnvironmentToConfigBundleNameMap, - InstanceContainerConfigDto, - InstanceDetails, - InstanceDto, + DeploymentWithConfigDto, + InstanceDetailsDto, InstanceSecretsDto, } from './deploy.dto' -import { DeploymentEventMessage } from './deploy.message' +import { + DeploymentBundlesUpdatedMessage, + DeploymentEventMessage, + InstanceDeletedMessage, + InstancesAddedMessage, +} from './deploy.message' @Injectable() export default class DeployMapper { constructor( - @Inject(forwardRef(() => ImageMapper)) - private imageMapper: ImageMapper, - private containerMapper: ContainerMapper, - @Inject(forwardRef(() => ProjectMapper)) - private projectMapper: ProjectMapper, - private auditMapper: AuditMapper, - @Inject(forwardRef(() => VersionMapper)) - private versionMapper: VersionMapper, - @Inject(forwardRef(() => NodeMapper)) - private nodeMapper: NodeMapper, - private encryptionService: EncryptionService, + @Inject(forwardRef(() => AgentService)) + private readonly agentService: AgentService, + private readonly imageMapper: ImageMapper, + @Inject(forwardRef(() => ContainerMapper)) + private readonly containerMapper: ContainerMapper, + private readonly projectMapper: ProjectMapper, + private readonly auditMapper: AuditMapper, + private readonly versionMapper: VersionMapper, + private readonly nodeMapper: NodeMapper, + @Inject(forwardRef(() => ConfigBundleMapper)) + private readonly configBundleMapper: ConfigBundleMapper, + private readonly encryptionService: EncryptionService, ) {} statusToDto(it: DeploymentStatusEnum): DeploymentStatusDto { @@ -89,6 +120,15 @@ export default class DeployMapper { } } + statusDtoToDb(it: DeploymentStatusDto): DeploymentStatusEnum { + switch (it) { + case 'in-progress': + return 'inProgress' + default: + return it as DeploymentStatusEnum + } + } + toDeploymentWithBasicNodeDto(it: DeploymentWithNode, nodeStatus: NodeConnectionStatus): DeploymentWithBasicNodeDto { return { id: it.id, @@ -125,100 +165,103 @@ export default class DeployMapper { } } - toDetailsDto( - deployment: DeploymentDetails, - publicKey?: string, - configBundleEnvironment?: EnvironmentToConfigBundleNameMap, - ): DeploymentDetailsDto { + toDeploymentWithConfigDto(deployment: DeploymentWithConfigAndBundles): DeploymentWithConfigDto { + const agent = this.agentService.getById(deployment.nodeId) + return { ...this.toDto(deployment), - token: deployment.tokens.length > 0 ? deployment.tokens[0] : null, + publicKey: agent?.publicKey ?? null, + configBundles: deployment.configBundles.map(it => this.configBundleMapper.detailsToDto(it.configBundle)), + config: this.instanceConfigToDto(deployment.config), + } + } + + toDetailsDto(deployment: DeploymentDetails): DeploymentDetailsDto { + return { + ...this.toDeploymentWithConfigDto(deployment), + token: deployment.token ?? null, lastTry: deployment.tries, - publicKey, - configBundleIds: deployment.configBundles.map(it => it.configBundle.id), - environment: deployment.environment as UniqueKeyValue[], instances: deployment.instances.map(it => this.instanceToDto(it)), - configBundleEnvironment: configBundleEnvironment ?? {}, } } - instanceToDto(it: InstanceDetails): InstanceDto { + instanceToDto(it: InstanceDetails): InstanceDetailsDto { return { id: it.id, - updatedAt: it.updatedAt, - image: this.imageMapper.toDto(it.image), - config: this.instanceConfigToDto(it.config as any as InstanceContainerConfigData), + updatedAt: it.config.updatedAt, + image: this.imageMapper.toDetailsDto(it.image), + config: this.instanceConfigToDto(it.config), } } - secretsResponseToInstanceSecretsDto(it: ListSecretsResponse): InstanceSecretsDto { + secretsResponseToDeploymentSecretsDto(it: ListSecretsResponse): DeploymentSecretsDto { return { - container: { - prefix: it.prefix, - name: it.name, - }, publicKey: it.publicKey, - keys: !it.hasKeys ? null : it.keys, + keys: it.keys, } } - instanceConfigToDto(it?: InstanceContainerConfigData): InstanceContainerConfigDto { - if (!it) { - return null - } - + secretsResponseToInstanceSecretsDto(it: ListSecretsResponse): InstanceSecretsDto { return { - ...this.containerMapper.configDataToDto(it as ContainerConfigData), - secrets: it.secrets, + ...this.secretsResponseToDeploymentSecretsDto(it), + container: it.target.container, } } - instanceConfigDtoToInstanceContainerConfigData( - imageConfig: ContainerConfigData, - currentConfig: InstanceContainerConfigData, - patch: InstanceContainerConfigDto, - ): InstanceContainerConfigData { - const config = this.containerMapper.configDtoToConfigData(currentConfig as ContainerConfigData, patch) + instanceConfigToDto(it: ContainerConfig): ConcreteContainerConfigDto { + const concreteConf = it as any as ConcreteContainerConfigData - if (config.labels) { - const currentLabels = currentConfig.labels ?? imageConfig.labels ?? {} + return { + ...this.containerMapper.configDataToDto(it.id, 'instance', it as any as ContainerConfigData), + secrets: concreteConf.secrets, + } + } - config.labels = { - deployment: config.labels.deployment ?? currentLabels.deployment, - ingress: config.labels.ingress ?? currentLabels.ingress, - service: config.labels.service ?? currentLabels.service, - } + dbDeploymentToCreateDeploymentStatement( + deployment: Deployment, + ): Omit { + const result = { + ...deployment, } - if (config.annotations) { - const currentAnnotations = currentConfig.annotations ?? imageConfig.annotations ?? {} + delete result.id + delete result.nodeId + delete result.versionId + delete result.configId - config.annotations = { - deployment: config.annotations.deployment ?? currentAnnotations.deployment, - ingress: config.annotations.ingress ?? currentAnnotations.ingress, - service: config.annotations.service ?? currentAnnotations.service, - } - } + return result + } - let secrets = !patch.secrets ? currentConfig.secrets : patch.secrets - if (secrets && !currentConfig.secrets && imageConfig.secrets) { - secrets = this.containerMapper.mergeSecrets(secrets, imageConfig.secrets) + concreteConfigDtoToConcreteContainerConfigData( + baseConfig: ContainerConfigData, + concreteConfig: ConcreteContainerConfigData, + patch: ConcreteContainerConfigDataDto, + ): ConcreteContainerConfigData { + const config = this.containerMapper.configDtoToConfigData( + concreteConfig as ContainerConfigData, + patch as ConcreteContainerConfigDto, + ) + + if ('labels' in patch) { + const currentLabels = concreteConfig.labels ?? baseConfig.labels ?? {} + config.labels = mergeMarkers(config.labels, currentLabels) } - return { - ...config, - secrets, + if ('annotations' in patch) { + const currentAnnotations = concreteConfig.annotations ?? baseConfig.annotations ?? {} + config.annotations = mergeMarkers(config.annotations, currentAnnotations) } - } - instanceConfigDataToDb(config: InstanceContainerConfigData): Omit { - const imageConfig = this.containerMapper.configDataToDb(config) - return { - ...imageConfig, - tty: config.tty, - useLoadBalancer: config.useLoadBalancer, - proxyHeaders: config.proxyHeaders, + if ('secrets' in patch) { + // when they are already overridden, we simply use the patch + // otherwise we need to merge with them with the image secrets + return { + ...config, + secrets: concreteConfig.secrets ? patch.secrets : mergeSecrets(patch.secrets, baseConfig.secrets), + } } + + return config as ConcreteContainerConfigData } eventTypeToDto(it: DeploymentEventTypeEnum): DeploymentEventTypeDto { @@ -310,42 +353,32 @@ export default class DeployMapper { return events } - deploymentToAgentInstanceConfig(deployment: Deployment, mergedEnvironment: UniqueKeyValue[]): InstanceConfig { - const environmentMap = this.mapKeyValueToMap(mergedEnvironment) - - return { - prefix: deployment.prefix, - environment: environmentMap, - } - } - containerStateToDto(state?: ProtoContainerState): ContainerState { return state ? (containerStateToJSON(state).toLowerCase() as ContainerState) : null } - commonConfigToAgentProto(config: MergedContainerConfigData, storage?: Storage): CommonContainerConfig { + commonConfigToAgentProto(config: ConcreteContainerConfigData, storage: Storage): CommonContainerConfig { return { name: config.name, environment: this.mapKeyValueToMap(config.environment), secrets: this.mapKeyValueToMap(config.secrets), commands: this.mapUniqueKeyToStringArray(config.commands), - expose: this.imageMapper.exposeStrategyToProto(config.expose) ?? ProtoExposeStrategy.NONE_ES, + expose: this.exposeStrategyToProto(config.expose), args: this.mapUniqueKeyToStringArray(config.args), - // Set user to the given value, if not null or use 0 if specifically 0, otherwise set null - user: config.user === -1 ? null : config.user, + user: config.user, workingDirectory: config.workingDirectory, TTY: config.tty, configContainer: config.configContainer, - importContainer: config.storageId ? this.storageToImportContainer(config, storage) : null, + importContainer: config.storageSet ? this.storageToImportContainer(config, storage) : null, routing: config.routing, initContainers: this.mapInitContainerToAgent(config.initContainers), - portRanges: config.portRanges, - ports: config.ports, - volumes: this.imageMapper.volumesToProto(config.volumes ?? []), + portRanges: config.portRanges ?? [], + ports: config.ports ?? [], + volumes: this.volumesToProto(config.volumes), } } - dagentConfigToAgentProto(config: MergedContainerConfigData): DagentContainerConfig { + dagentConfigToAgentProto(config: ConcreteContainerConfigData): DagentContainerConfig { return { networks: this.mapUniqueKeyToStringArray(config.networks), logConfig: @@ -353,28 +386,28 @@ export default class DeployMapper { ? null : { ...config.logConfig, - driver: this.imageMapper.logDriverToProto(config.logConfig.driver), + driver: this.logDriverToProto(config.logConfig.driver), options: this.mapKeyValueToMap(config.logConfig.options), }, - networkMode: this.imageMapper.networkModeToProto(config.networkMode), - restartPolicy: this.imageMapper.restartPolicyToProto(config.restartPolicy), + networkMode: this.networkModeToProto(config.networkMode), + restartPolicy: this.restartPolicyToProto(config.restartPolicy), labels: this.mapKeyValueToMap(config.dockerLabels), expectedState: !config.expectedState ? null : { - state: this.imageMapper.stateToProto(config.expectedState.state), + state: this.stateToProto(config.expectedState.state), timeout: config.expectedState.timeout, exitCode: config.expectedState.exitCode, }, } } - craneConfigToAgentProto(config: MergedContainerConfigData): CraneContainerConfig { + craneConfigToAgentProto(config: ConcreteContainerConfigData): CraneContainerConfig { return { customHeaders: this.mapUniqueKeyToStringArray(config.customHeaders), extraLBAnnotations: this.mapKeyValueToMap(config.extraLBAnnotations), deploymentStrategy: - this.imageMapper.deploymentStrategyToProto(config.deploymentStrategy) ?? ProtoDeploymentStrategy.ROLLING_UPDATE, + this.deploymentStrategyToProto(config.deploymentStrategy) ?? ProtoDeploymentStrategy.ROLLING_UPDATE, healthCheckConfig: config.healthCheckConfig, proxyHeaders: config.proxyHeaders, useLoadBalancer: config.useLoadBalancer, @@ -405,10 +438,35 @@ export default class DeployMapper { } } + instancesCreatedEventToMessage(event: InstancesCreatedEvent): InstancesAddedMessage { + return event.instances.map(it => ({ + id: it.id, + configId: it.configId, + image: this.imageMapper.toDetailsDto(it.image), + })) + } + + instanceDeletedEventToMessage(event: InstanceDeletedEvent): InstanceDeletedMessage { + return { + instanceId: event.id, + configId: event.configId, + } + } + + bundlesUpdatedEventToMessage(event: DeploymentConfigBundlesUpdatedEvent): DeploymentBundlesUpdatedMessage { + return { + bundles: event.bundles.map(it => this.configBundleMapper.toDto(it)), + } + } + private mapInitContainerToAgent(list: InitContainer[]): AgentInitContainer[] { + if (!list) { + return [] + } + const result: AgentInitContainer[] = [] - list?.forEach(it => { + list.forEach(it => { result.push({ ...it, environment: this.mapKeyValueToMap(it.environment as KeyValue[]), @@ -442,7 +500,7 @@ export default class DeployMapper { return list.map(it => it.key) } - private storageToImportContainer(config: MergedContainerConfigData, storage: Storage): ImportContainer { + private storageToImportContainer(config: ConcreteContainerConfigData, storage: Storage): ImportContainer { const url = /^(http)s?/.test(storage.url) ? storage.url : `https://${storage.url}` let environment: { [key: string]: string } = { RCLONE_CONFIG_S3_TYPE: 's3', @@ -463,4 +521,107 @@ export default class DeployMapper { environment, } } + + private logDriverToProto(it: ContainerLogDriverType): DriverType { + switch (it) { + case undefined: + case null: + case 'none': + return DriverType.DRIVER_TYPE_NONE + case 'json-file': + return DriverType.JSON_FILE + default: + return driverTypeFromJSON(it.toUpperCase()) + } + } + + private volumesToProto(volumes: Volume[]): ProtoVolume[] { + if (!volumes) { + return [] + } + + return volumes.map(it => ({ ...it, type: this.volumeTypeToProto(it.type) })) + } + + private volumeTypeToProto(it?: ContainerVolumeType): ProtoVolumeType { + if (!it) { + return ProtoVolumeType.RO + } + + return volumeTypeFromJSON(it.toUpperCase()) + } + + private stateToProto(state: ContainerState): ProtoContainerState { + if (!state) { + return null + } + + switch (state) { + case 'running': + return ProtoContainerState.RUNNING + case 'waiting': + return ProtoContainerState.WAITING + case 'exited': + return ProtoContainerState.EXITED + default: + return ProtoContainerState.CONTAINER_STATE_UNSPECIFIED + } + } + + private exposeStrategyToProto(type: ExposeStrategy): ProtoExposeStrategy { + if (!type) { + return ProtoExposeStrategy.NONE_ES + } + + switch (type) { + case ExposeStrategy.expose: + return ProtoExposeStrategy.EXPOSE + case ExposeStrategy.exposeWithTls: + return ProtoExposeStrategy.EXPOSE_WITH_TLS + default: + return ProtoExposeStrategy.NONE_ES + } + } + + private restartPolicyToProto(type: RestartPolicy): ProtoRestartPolicy { + if (!type) { + return null + } + + switch (type) { + case RestartPolicy.always: + return ProtoRestartPolicy.ALWAYS + case RestartPolicy.no: + return ProtoRestartPolicy.NO + case RestartPolicy.unlessStopped: + return ProtoRestartPolicy.UNLESS_STOPPED + case RestartPolicy.onFailure: + return ProtoRestartPolicy.ON_FAILURE + default: + return ProtoRestartPolicy.NO + } + } + + private deploymentStrategyToProto(type: DeploymentStrategy): ProtoDeploymentStrategy { + if (!type) { + return null + } + + switch (type) { + case DeploymentStrategy.recreate: + return ProtoDeploymentStrategy.RECREATE + case DeploymentStrategy.rolling: + return ProtoDeploymentStrategy.ROLLING_UPDATE + default: + return ProtoDeploymentStrategy.DEPLOYMENT_STRATEGY_UNSPECIFIED + } + } + + private networkModeToProto(it: NetworkMode): ProtoNetworkMode { + if (!it) { + return null + } + + return networkModeFromJSON(it?.toUpperCase()) + } } diff --git a/web/crux/src/app/deploy/deploy.message.ts b/web/crux/src/app/deploy/deploy.message.ts index 301d1413f2..9575ce4b5f 100644 --- a/web/crux/src/app/deploy/deploy.message.ts +++ b/web/crux/src/app/deploy/deploy.message.ts @@ -1,6 +1,6 @@ -import { InstanceContainerConfigData, UniqueKeyValue } from 'src/domain/container' -import { ImageConfigProperty } from '../image/image.const' -import { DeploymentEventDto, EnvironmentToConfigBundleNameMap, InstanceDetails, InstanceDto } from './deploy.dto' +import { ConfigBundleDto } from '../config.bundle/config.bundle.dto' +import { ImageDetailsDto } from '../image/image.dto' +import { DeploymentEventDto } from './deploy.dto' export const WS_TYPE_FETCH_DEPLOYMENT_EVENTS = 'fetch-deployment-events' @@ -10,51 +10,22 @@ export type DeploymentEventMessage = DeploymentEventDto export const WS_TYPE_DEPLOYMENT_EVENT_LIST = 'deployment-event-list' export type DeploymentEventListMessage = DeploymentEventMessage[] -export const WS_TYPE_PATCH_INSTANCE = 'patch-instance' -export type PatchInstanceMessage = { - instanceId: string - config?: Partial - resetSection?: ImageConfigProperty -} - -export const WS_TYPE_PATCH_RECEIVED = 'patch-received' - -export const WS_TYPE_INSTANCE_UPDATED = 'instance-updated' -export type InstanceUpdatedMessage = InstanceContainerConfigData & { - instanceId: string -} - -export const WS_TYPE_PATCH_DEPLOYMENT_ENV = 'patch-deployment-env' -export type PatchDeploymentEnvMessage = { - environment?: UniqueKeyValue[] - configBundleIds?: string[] +export const WS_TYPE_DEPLOYMENT_BUNDLES_UPDATED = 'deployment-bundles-updated' +export type DeploymentBundlesUpdatedMessage = { + bundles: ConfigBundleDto[] } -export const WS_TYPE_DEPLOYMENT_ENV_UPDATED = 'deployment-env-updated' -export type DeploymentEnvUpdatedMessage = { - environment?: UniqueKeyValue[] - configBundleIds?: string[] - configBundleEnvironment?: EnvironmentToConfigBundleNameMap -} - -export const WS_TYPE_GET_INSTANCE = 'get-instance' -export type GetInstanceMessage = { +type InstanceCreatedMessage = { id: string + configId: string + image: ImageDetailsDto } -export const WS_TYPE_INSTANCE = 'instance' -export type InstanceMessage = InstanceDto - -export const WS_TYPE_GET_INSTANCE_SECRETS = 'get-instance-secrets' -export type GetInstanceSecretsMessage = { - id: string -} +export const WS_TYPE_INSTANCES_ADDED = 'instances-added' +export type InstancesAddedMessage = InstanceCreatedMessage[] -export const WS_TYPE_INSTANCE_SECRETS = 'instance-secrets' -export type InstanceSecretsMessage = { +export const WS_TYPE_INSTANCE_DELETED = 'instance-deleted' +export type InstanceDeletedMessage = { instanceId: string - keys: string[] + configId: string } - -export const WS_TYPE_INSTANCES_ADDED = 'instances-added' -export type InstancesAddedMessage = InstanceDetails[] diff --git a/web/crux/src/app/deploy/deploy.module.ts b/web/crux/src/app/deploy/deploy.module.ts index ae94bb1a90..bd292ec0d9 100644 --- a/web/crux/src/app/deploy/deploy.module.ts +++ b/web/crux/src/app/deploy/deploy.module.ts @@ -7,6 +7,7 @@ import PrismaService from 'src/services/prisma.service' import AgentModule from '../agent/agent.module' import AuditLoggerModule from '../audit.logger/audit.logger.module' import AuditMapper from '../audit/audit.mapper' +import ConfigBundleModule from '../config.bundle/config.bundle.module' import ContainerModule from '../container/container.module' import EditorModule from '../editor/editor.module' import ImageModule from '../image/image.module' @@ -15,6 +16,7 @@ import ProjectMapper from '../project/project.mapper' import RegistryModule from '../registry/registry.module' import TeamRepository from '../team/team.repository' import VersionMapper from '../version/version.mapper' +import DeployDomainEventListener from './deploy.domain-event.listener' import DeployHttpController from './deploy.http.controller' import { DeployJwtStrategy } from './deploy.jwt.strategy' import DeployMapper from './deploy.mapper' @@ -24,11 +26,12 @@ import DeployWebSocketGateway from './deploy.ws.gateway' @Module({ imports: [ forwardRef(() => AgentModule), - ImageModule, + forwardRef(() => ImageModule), EditorModule, RegistryModule, - ContainerModule, + forwardRef(() => ContainerModule), ConfigModule, + ConfigBundleModule, AuditLoggerModule, ...CruxJwtModuleImports, ], @@ -37,6 +40,7 @@ import DeployWebSocketGateway from './deploy.ws.gateway' providers: [ PrismaService, DeployService, + DeployDomainEventListener, DeployMapper, TeamRepository, KratosService, diff --git a/web/crux/src/app/deploy/deploy.service.ts b/web/crux/src/app/deploy/deploy.service.ts index 07ecc46346..30bc2ee158 100644 --- a/web/crux/src/app/deploy/deploy.service.ts +++ b/web/crux/src/app/deploy/deploy.service.ts @@ -1,32 +1,49 @@ import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common' import { ConfigService } from '@nestjs/config' +import { EventEmitter2 } from '@nestjs/event-emitter' import { JwtService } from '@nestjs/jwt' import { Identity } from '@ory/kratos-client' -import { ConfigBundle, DeploymentStatusEnum, Prisma } from '@prisma/client' -import { EMPTY, Observable, Subject, concatAll, concatMap, filter, from, map, of } from 'rxjs' +import { ContainerConfig, DeploymentStatusEnum, Prisma } from '@prisma/client' +import { EMPTY, Observable, filter, map } from 'rxjs' import { + ConcreteContainerConfigData, ContainerConfigData, - InstanceContainerConfigData, - MergedContainerConfigData, - UniqueKeyValue, UniqueSecretKeyValue, + configIsEmpty, } from 'src/domain/container' -import Deployment from 'src/domain/deployment' +import Deployment, { DeploymentWithConfig } from 'src/domain/deployment' import { DeploymentTokenScriptGenerator } from 'src/domain/deployment-token' +import { + DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE, + DEPLOYMENT_EVENT_INSTACE_CREATE, + DEPLOYMENT_EVENT_INSTACE_DELETE, + DeploymentConfigBundlesUpdatedEvent, + InstanceDeletedEvent, + InstancesCreatedEvent, +} from 'src/domain/domain-events' +import { + InvalidSecrets, + collectInvalidSecrets, + deploymentConfigOf, + instanceConfigOf, + mergePrefixNeighborSecrets, +} from 'src/domain/start-deployment' import { DeploymentTokenPayload, tokenSignOptionsFor } from 'src/domain/token' import { collectChildVersionIds, collectParentVersionIds, toPrismaJson } from 'src/domain/utils' +import { copyDeployment } from 'src/domain/version-increase' import { CruxPreconditionFailedException } from 'src/exception/crux-exception' -import { DeployRequest } from 'src/grpc/protobuf/proto/agent' +import { DeployWorkloadRequest } from 'src/grpc/protobuf/proto/agent' import EncryptionService from 'src/services/encryption.service' import PrismaService from 'src/services/prisma.service' +import { DomainEvent } from 'src/shared/domain-event' +import { WsMessage } from 'src/websockets/common' import { v4 as uuid } from 'uuid' import AgentService from '../agent/agent.service' import ContainerMapper from '../container/container.mapper' import { EditorLeftMessage, EditorMessage } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' -import { ImageEvent } from '../image/image.event' -import ImageEventService from '../image/image.event.service' import RegistryMapper from '../registry/registry.mapper' +import DeployDomainEventListener from './deploy.domain-event.listener' import { CopyDeploymentDto, CreateDeploymentDto, @@ -34,50 +51,51 @@ import { DeploymentDetailsDto, DeploymentDto, DeploymentEventDto, - DeploymentImageEvent, + DeploymentListDto, DeploymentLogListDto, DeploymentLogPaginationQuery, + DeploymentQueryDto, + DeploymentSecretsDto, DeploymentTokenCreatedDto, - EnvironmentToConfigBundleNameMap, - InstanceDto, + InstanceDetailsDto, InstanceSecretsDto, - PatchDeploymentDto, PatchInstanceDto, + UpdateDeploymentDto, } from './deploy.dto' import DeployMapper from './deploy.mapper' -import { DeploymentEventListMessage } from './deploy.message' +import { + DeploymentEventListMessage, + WS_TYPE_DEPLOYMENT_BUNDLES_UPDATED, + WS_TYPE_INSTANCES_ADDED, + WS_TYPE_INSTANCE_DELETED, +} from './deploy.message' @Injectable() export default class DeployService { private readonly logger = new Logger(DeployService.name) - private deploymentImageEvents = new Subject() - constructor( private readonly prisma: PrismaService, private readonly jwtService: JwtService, - @Inject(forwardRef(() => AgentService)) private readonly agentService: AgentService, - readonly imageEventService: ImageEventService, + @Inject(forwardRef(() => AgentService)) + private readonly agentService: AgentService, + private readonly domainEvents: DeployDomainEventListener, + @Inject(forwardRef(() => DeployMapper)) private readonly mapper: DeployMapper, + private readonly events: EventEmitter2, private readonly registryMapper: RegistryMapper, + @Inject(forwardRef(() => ContainerMapper)) private readonly containerMapper: ContainerMapper, private readonly editorServices: EditorServiceProvider, private readonly configService: ConfigService, private readonly encryptionService: EncryptionService, - ) { - imageEventService - .watchEvents() - .pipe( - concatMap(async it => { - const event = await this.transformImageEvent(it) - if (event.type === 'create') { - return from(this.onImageAddedToVersion(event)) - } - return of(event) - }), - concatAll(), - ) - .subscribe(it => this.deploymentImageEvents.next(it)) + ) {} + + subscribeToDomainEvents(deploymentId: string): Observable { + return this.domainEvents.watchEvents(deploymentId).pipe( + map(it => this.transformDomainEventToWsMessage(it)), + filter(it => !!it), + ) } async checkDeploymentIsInTeam(teamSlug: string, deploymentId: string, identity: Identity): Promise { @@ -109,9 +127,14 @@ export default class DeployService { }, include: { node: true, + config: true, configBundles: { include: { - configBundle: true, + configBundle: { + include: { + config: true, + }, + }, }, }, version: { @@ -119,7 +142,7 @@ export default class DeployService { project: true, }, }, - tokens: { + token: { select: { id: true, name: true, @@ -146,12 +169,7 @@ export default class DeployService { }, }) - const publicKey = this.agentService.getById(deployment.nodeId)?.publicKey - const configBundleEnvironment = this.getConfigBundleEnvironmentKeys( - deployment.configBundles.map(it => it.configBundle), - ) - - return this.mapper.toDetailsDto(deployment, publicKey, configBundleEnvironment) + return this.mapper.toDetailsDto(deployment) } async getDeploymentEvents(deploymentId: string, tryCount?: number): Promise { @@ -168,7 +186,7 @@ export default class DeployService { return events.map(it => this.mapper.eventToDto(it)) } - async getInstance(instanceId: string): Promise { + async getInstance(instanceId: string): Promise { const instance = await this.prisma.instance.findUniqueOrThrow({ where: { id: instanceId, @@ -199,56 +217,11 @@ export default class DeployService { }, }) - const deployment = await this.prisma.deployment.create({ - data: { - versionId: request.versionId, - nodeId: request.nodeId, - status: DeploymentStatusEnum.preparing, - note: request.note, - createdBy: identity.id, - prefix: request.prefix, - protected: request.protected, - instances: { - createMany: { - data: version.images.map(it => ({ - imageId: it.id, - })), - }, - }, - }, - include: { - node: true, - version: { - include: { - project: true, - }, - }, - }, - }) - - const instanceIds = await this.prisma.instance.findMany({ - where: { - deploymentId: deployment.id, - }, - select: { - id: true, - imageId: true, - image: { - select: { - name: true, - }, - }, - }, - }) - - const previousInstances = await this.prisma.deployment.findFirst({ + const previousDeployment = await this.prisma.deployment.findFirst({ where: { prefix: request.prefix, nodeId: request.nodeId, versionId: request.versionId, - id: { - not: deployment.id, - }, }, include: { instances: { @@ -263,78 +236,135 @@ export default class DeployService { }, }) - if (previousInstances && previousInstances.instances) { - instanceIds.forEach(async it => { - await this.prisma.instance.update({ - where: { - id: it.id, + const deploy = await this.prisma.$transaction(async prisma => { + const deployment = await this.prisma.deployment.create({ + data: { + version: { connect: { id: request.versionId } }, + node: { connect: { id: request.nodeId } }, + status: DeploymentStatusEnum.preparing, + note: request.note, + createdBy: identity.id, + prefix: request.prefix, + protected: request.protected, + config: { create: { type: 'deployment' } }, + }, + include: { + node: true, + version: { + include: { + project: true, + }, }, - data: { - config: { - create: { - secrets: - previousInstances.instances.find(instance => instance.imageId === it.imageId).config?.secrets ?? - Prisma.JsonNull, + instances: { + select: { + id: true, + imageId: true, + image: { + select: { + name: true, + }, }, }, }, - }) + }, }) - } - return this.mapper.toDto(deployment) - } + const instances = await Promise.all( + version.images.map( + async image => + await prisma.instance.create({ + data: { + deployment: { connect: { id: deployment.id } }, + image: { connect: { id: image.id } }, + config: { create: { type: 'instance' } }, + }, + select: { + id: true, + imageId: true, + image: { + select: { + name: true, + }, + }, + }, + }), + ), + ) - async patchDeployment(deploymentId: string, req: PatchDeploymentDto, identity: Identity): Promise { - if (req.configBundleIds) { - const connections = await this.prisma.deployment.findFirst({ - where: { - id: deploymentId, - }, - include: { - configBundles: true, - }, - }) + deployment.instances = instances - const connectedBundles = connections.configBundles.map(it => it.configBundleId) - const toConnect = req.configBundleIds.filter(it => !connectedBundles.includes(it)) - const toDisconnect = connectedBundles.filter(it => !req.configBundleIds.includes(it)) - - if (toConnect.length > 0 || toDisconnect.length > 0) { - await this.prisma.$transaction(async prisma => { - await prisma.configBundleOnDeployments.createMany({ - data: toConnect.map(it => ({ - deploymentId, - configBundleId: it, - })), + const previousSecrets: Map = new Map( + previousDeployment?.instances + ?.filter(it => { + const secrets = it.config.secrets as UniqueSecretKeyValue[] + return !!it.config.secrets || secrets.length > 0 }) + ?.map(it => [it.imageId, it.config.secrets as UniqueSecretKeyValue[]]) ?? [], + ) + + await Promise.all( + deployment.instances.map(async it => { + const secrets = previousSecrets[it.imageId] - await prisma.configBundleOnDeployments.deleteMany({ + await prisma.instance.update({ where: { - deploymentId, - configBundleId: { - in: toDisconnect, + id: it.id, + }, + data: { + config: { + create: { + type: 'instance', + updatedBy: identity.id, + secrets, + }, }, }, }) - }) - } - } + }), + ) - await this.prisma.deployment.update({ + return deployment + }) + + return this.mapper.toDto(deploy) + } + + async updateDeployment(deploymentId: string, req: UpdateDeploymentDto, identity: Identity): Promise { + const deployment = await this.prisma.deployment.update({ where: { id: deploymentId, }, data: { - note: req.note ?? undefined, - prefix: req.prefix ?? undefined, - protected: req.protected ?? undefined, - environment: req.environment - ? req.environment.map(it => this.containerMapper.uniqueKeyValueDtoToDb(it)) - : undefined, + note: req.note, + prefix: req.prefix, + protected: req.protected, + configBundles: { + deleteMany: { + deploymentId, + }, + create: req.configBundles.map(configBundleId => ({ configBundle: { connect: { id: configBundleId } } })), + }, updatedBy: identity.id, }, + select: { + configBundles: { + select: { + configBundle: { + include: { + config: true, + }, + }, + }, + }, + }, }) + + const event: DeploymentConfigBundlesUpdatedEvent = { + deploymentId, + bundles: deployment.configBundles.map(it => it.configBundle), + } + + await this.events.emitAsync(DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE, event) } async patchInstance( @@ -357,18 +387,18 @@ export default class DeployService { }, }) - const configData = this.mapper.instanceConfigDtoToInstanceContainerConfigData( + const configData = this.mapper.concreteConfigDtoToConcreteContainerConfigData( instance.image.config as any as ContainerConfigData, - (instance.config ?? {}) as any as InstanceContainerConfigData, + (instance.config ?? {}) as any as ConcreteContainerConfigData, req.config, ) - const config = this.containerMapper.configDataToDb(configData) - - // We should overwrite the user in the ConfigData. This is an edge case, which is why we haven't - // implemented a new mapper for configDataToDb. However, in the long run, if there are more similar - // situations, we will have to create a different mapper for InstanceConfig. - config.user = configData.user + const config: Omit = { + ...this.containerMapper.configDataToDbPatch(configData), + type: 'deployment', + updatedAt: new Date(), + updatedBy: identity.id, + } await this.prisma.deployment.update({ where: { @@ -383,10 +413,7 @@ export default class DeployService { }, data: { config: { - upsert: { - update: config, - create: config, - }, + update: config, }, }, }, @@ -396,7 +423,7 @@ export default class DeployService { } async deleteDeployment(deploymentId: string): Promise { - const deployment = await this.prisma.deployment.delete({ + await this.prisma.deployment.delete({ where: { id: deploymentId, }, @@ -406,18 +433,6 @@ export default class DeployService { status: true, }, }) - - if (deployment.status === 'successful') { - const agent = this.agentService.getById(deployment.nodeId) - if (!agent) { - return - } - - await agent.deleteContainers({ - prefix: deployment.prefix, - container: null, - }) - } } async startDeployment(deploymentId: string, identity: Identity, instances?: string[]): Promise { @@ -436,9 +451,14 @@ export default class DeployService { id: deploymentId, }, include: { + config: true, configBundles: { include: { - configBundle: true, + configBundle: { + include: { + config: true, + }, + }, }, }, version: { @@ -473,7 +493,7 @@ export default class DeployService { name: true, }, }, - tokens: { + token: { select: { name: true, }, @@ -492,98 +512,42 @@ export default class DeployService { }) } - const invalidSecrets = deployment.instances - .map(it => { - if (!it.config) { - return null - } + const invalidSecrets: InvalidSecrets[] = [] - const secrets = it.config.secrets as UniqueSecretKeyValue[] + // deployment config + const deploymentConfig = deploymentConfigOf(deployment) - if (!secrets || secrets.every(secret => secret.publicKey === publicKey)) { - return null - } + if (deploymentConfig.secrets) { + const invalidDeploymentSecrets = collectInvalidSecrets(deployment.configId, deploymentConfig, publicKey) - return { - instanceId: it.id, - invalid: secrets.filter(secret => secret.publicKey !== publicKey).map(secret => secret.id), - secrets: secrets.map(secret => { - if (secret.publicKey === publicKey) { - return secret - } - - return { - ...secret, - value: '', - encrypted: false, - publicKey, - } - }), - } - }) - .filter(it => it !== null) - - const invalidSecretsUpdates = invalidSecrets - .map(it => - this.prisma.instance.update({ - where: { - id: it.instanceId, - }, - data: { - config: { - update: { - secrets: it.secrets, - }, - }, - }, - }), - ) - .filter(it => it !== null) - - if (invalidSecretsUpdates.length > 0) { - await this.prisma.$transaction(invalidSecretsUpdates) - - throw new CruxPreconditionFailedException({ - message: 'Some secrets are invalid', - property: 'secrets', - value: invalidSecrets.map(it => ({ ...it, secrets: undefined })), - }) + if (invalidDeploymentSecrets) { + invalidSecrets.push(invalidDeploymentSecrets) + } } - const mergedConfigs: Map = new Map( - deployment.instances.map(it => { - /* - * If a deployment succeeds the merged config is saved as the instance config, - * so downgrading is possible even if the image config is modified. - */ - - if ( - deployment.version.type !== 'rolling' && - (deployment.status === 'successful' || deployment.status === 'obsolete') - ) { - return [ - it.id, - this.containerMapper.mergeConfigs( - {} as ContainerConfigData, - (it.config ?? {}) as InstanceContainerConfigData, - ), - ] - } + // instance config + // instanceId to instanceConfig + const instanceConfigs: Map = new Map( + deployment.instances.map(instance => { + const instanceConfig = instanceConfigOf(deployment, deploymentConfig, instance) - return [ - it.id, - this.containerMapper.mergeConfigs( - (it.image.config ?? {}) as ContainerConfigData, - (it.config ?? {}) as InstanceContainerConfigData, - ), - ] + return [instance.id, instanceConfig] }), ) - const mergedEnvironment = this.mergeEnvironments( - (deployment.environment as UniqueKeyValue[]) ?? [], - deployment.configBundles.map(it => it.configBundle), - ) + const invalidInstanceSecrets = deployment.instances + .map(it => collectInvalidSecrets(it.configId, instanceConfigs.get(it.id), publicKey)) + .filter(it => !!it) + + invalidSecrets.push(...invalidInstanceSecrets) + + // check for invalid secrets + if (invalidSecrets.length > 0) { + await this.updateInvalidSecretsAndThrow(invalidSecrets) + } + + const prefixNeighbors = await this.collectLatestSuccessfulDeploymentsForPrefix(deployment.nodeId, deployment.prefix) + const sharedSecrets = mergePrefixNeighborSecrets(prefixNeighbors, publicKey) const tries = deployment.tries + 1 await this.prisma.deployment.update({ @@ -595,34 +559,34 @@ export default class DeployService { }, }) - const deploy = new Deployment( - { + const deploy = new Deployment({ + request: { id: deployment.id, releaseNotes: deployment.version.changelog, versionName: deployment.version.name, + prefix: deployment.prefix, + secrets: sharedSecrets, requests: await Promise.all( deployment.instances.map(async it => { const { registry } = it.image const registryUrl = this.registryMapper.pullUrlOf(registry) - const mergedConfig = mergedConfigs.get(it.id) - const storage = mergedConfig.storageId + const config = instanceConfigs.get(it.id) + const storage = config.storageSet ? await this.prisma.storage.findFirstOrThrow({ where: { - id: mergedConfig.storageId, + id: config.storageId, }, }) : undefined return { - common: this.mapper.commonConfigToAgentProto(mergedConfig, storage), - crane: this.mapper.craneConfigToAgentProto(mergedConfig), - dagent: this.mapper.dagentConfigToAgentProto(mergedConfig), + common: this.mapper.commonConfigToAgentProto(config, storage), + crane: this.mapper.craneConfigToAgentProto(config), + dagent: this.mapper.dagentConfigToAgentProto(config), id: it.id, - containerName: it.image.config.name, imageName: it.image.name, tag: it.image.tag, - instanceConfig: this.mapper.deploymentToAgentInstanceConfig(deployment, mergedEnvironment), registry: registryUrl, registryAuth: !registry.user ? undefined @@ -632,21 +596,21 @@ export default class DeployService { user: registry.user, password: this.encryptionService.decrypt(registry.token), }, - } as DeployRequest + } as DeployWorkloadRequest }), ), }, - { + instanceConfigs, + notification: { teamId: deployment.version.project.teamId, - actor: identity ?? (deployment.tokens.length > 0 ? deployment.tokens[0].name : null), + actor: identity ?? deployment.token?.name ?? null, projectName: deployment.version.project.name, versionName: deployment.version.name, nodeName: deployment.node.name, }, - mergedConfigs, - mergedEnvironment, + deploymentConfig: !configIsEmpty(deploymentConfig) ? deploymentConfig : null, tries, - ) + }) this.logger.debug(`Starting deployment: ${deploy.id}`) @@ -658,6 +622,8 @@ export default class DeployService { }, data: { status: DeploymentStatusEnum.inProgress, + deployedAt: new Date(), + deployedBy: identity.id, }, }) } @@ -667,7 +633,7 @@ export default class DeployService { select: { status: true, prefix: true, - environment: true, + config: true, configBundles: { include: { configBundle: true, @@ -704,6 +670,7 @@ export default class DeployService { }, data: { status: finalStatus, + deployedAt: new Date(), }, }) @@ -715,6 +682,8 @@ export default class DeployService { } await this.prisma.$transaction(async prisma => { + // set other deployments to obsolate in this version + await prisma.deployment.updateMany({ data: { status: DeploymentStatusEnum.obsolete, @@ -732,6 +701,14 @@ export default class DeployService { }, }) + if (deployment.version.type === 'rolling') { + // we don't care about version parents and children + // also we don't save the concrete configs + + return + } + + // set other deployments obsolate in the parent version const parentVersionIds = await collectParentVersionIds(prisma, deployment.version.id) await prisma.deployment.updateMany({ data: { @@ -749,6 +726,7 @@ export default class DeployService { }, }) + // set other diployments in children to downgraded const childVersionIds = await collectChildVersionIds(prisma, deployment.version.id) await prisma.deployment.updateMany({ data: { @@ -763,17 +741,15 @@ export default class DeployService { }, }) - if (deployment.version.type === 'rolling') { - return - } - - if (finishedDeployment.sharedEnvironment.length > 0) { + if (finishedDeployment.deploymentConfig) { await prisma.deployment.update({ where: { id: finishedDeployment.id, }, data: { - environment: toPrismaJson(finishedDeployment.sharedEnvironment), + config: { + update: this.containerMapper.configDataToDbPatch(finishedDeployment.deploymentConfig), + }, }, }) @@ -784,21 +760,18 @@ export default class DeployService { }) } - const configUpserts = Array.from(finishedDeployment.mergedConfigs).map(it => { + const configUpserts = Array.from(finishedDeployment.instanceConfigs).map(it => { const [key, config] = it - const dbConfig = this.containerMapper.configDataToDb(config) + const data = this.containerMapper.configDataToDbPatch(config) - return prisma.instanceContainerConfig.upsert({ + return prisma.instance.update({ where: { - instanceId: key, + id: key, }, - update: { - ...dbConfig, - }, - create: { - ...dbConfig, - id: undefined, - instanceId: key, + data: { + config: { + update: data, + }, }, }) }) @@ -831,48 +804,135 @@ export default class DeployService { return runningDeployment.watchStatus().pipe(map(it => this.mapper.progressEventToEventDto(it))) } - subscribeToDeploymentEditEvents(deploymentId: string): Observable { - return this.deploymentImageEvents.pipe(filter(it => it.deploymentIds.includes(deploymentId))) - } - - async getDeployments(teamSlug: string, nodeId?: string): Promise { - const deployments = await this.prisma.deployment.findMany({ - where: { - version: { - project: { - team: { - slug: teamSlug, - }, + async getDeployments(teamSlug: string, query?: DeploymentQueryDto): Promise { + let where: Prisma.DeploymentWhereInput = { + version: { + project: { + team: { + slug: teamSlug, }, }, - nodeId, }, - include: { - version: { - select: { - id: true, - name: true, - type: true, - project: { - select: { - id: true, - name: true, - type: true, + nodeId: query.nodeId, + status: query.status ? this.mapper.statusDtoToDb(query.status) : undefined, + } + + if (query.configBundleId) { + where = { + ...where, + configBundles: { + some: { + configBundleId: query.configBundleId, + }, + }, + } + } + + if (query.filter) { + const { filter: filterKeyword } = query + where = { + ...where, + OR: [ + { + prefix: { + contains: filterKeyword, + mode: 'insensitive', + }, + }, + { + node: { + name: { + contains: filterKeyword, + mode: 'insensitive', + }, + }, + }, + { + version: { + name: { + contains: filterKeyword, + mode: 'insensitive', }, }, }, + { + version: { + project: { + name: { + contains: filterKeyword, + mode: 'insensitive', + }, + }, + }, + }, + ], + } + } + + const [deployments, total] = await this.prisma.$transaction([ + this.prisma.deployment.findMany({ + where, + orderBy: { + createdAt: 'desc', }, - node: { - select: { - id: true, - name: true, - type: true, + skip: query.skip, + take: query.take, + include: { + version: { + select: { + id: true, + name: true, + type: true, + project: { + select: { + id: true, + name: true, + type: true, + }, + }, + }, + }, + node: { + select: { + id: true, + name: true, + type: true, + }, }, }, + }), + this.prisma.deployment.count({ where }), + ]) + + return { + items: deployments.map(it => this.mapper.toDto(it)), + total, + } + } + + async getDeploymentSecrets(deploymentId: string): Promise { + const deployment = await this.prisma.deployment.findUniqueOrThrow({ + where: { + id: deploymentId, + }, + }) + + const agent = this.agentService.getById(deployment.nodeId) + if (!agent) { + throw new CruxPreconditionFailedException({ + message: 'Node is unreachable', + property: 'nodeId', + value: deployment.nodeId, + }) + } + + const secrets = await agent.listSecrets({ + target: { + prefix: deployment.prefix, }, }) - return deployments.map(it => this.mapper.toDto(it)) + return this.mapper.secretsResponseToDeploymentSecretsDto(secrets) } async getInstanceSecrets(instanceId: string): Promise { @@ -903,9 +963,11 @@ export default class DeployService { } const secrets = await agent.listSecrets({ - container: { - prefix: deployment.prefix, - name: containerName, + target: { + container: { + prefix: deployment.prefix, + name: containerName, + }, }, }) @@ -918,6 +980,7 @@ export default class DeployService { id: deploymentId, }, include: { + config: true, instances: { include: { config: true, @@ -926,15 +989,29 @@ export default class DeployService { }, }) + const copiedDeployment = copyDeployment(oldDeployment) + const newDeployment = await this.prisma.deployment.create({ data: { - versionId: oldDeployment.versionId, - nodeId: request.nodeId, prefix: request.prefix, note: request.note, status: DeploymentStatusEnum.preparing, createdBy: identity.id, - environment: oldDeployment.environment ?? [], + version: { + connect: { + id: copiedDeployment.versionId, + }, + }, + node: { + connect: { + id: copiedDeployment.nodeId, + }, + }, + config: !copiedDeployment.config + ? undefined + : { + create: this.containerMapper.dbConfigToCreateConfigStatement(copiedDeployment.config), + }, }, include: { node: true, @@ -952,45 +1029,24 @@ export default class DeployService { oldDeployment.instances.map(it => this.prisma.instance.create({ data: { - deploymentId: newDeployment.id, - imageId: it.imageId, - config: it.config - ? { + deployment: { + connect: { + id: newDeployment.id, + }, + }, + image: { + connect: { + id: it.imageId, + }, + }, + config: !it.config + ? undefined + : { create: { - name: it.config.name, - expose: it.config.expose, - routing: toPrismaJson(it.config.routing), - configContainer: toPrismaJson(it.config.configContainer), - user: it.config.user, - tty: it.config.tty, - ports: toPrismaJson(it.config.ports), - portRanges: toPrismaJson(it.config.portRanges), - volumes: toPrismaJson(it.config.volumes), - commands: toPrismaJson(it.config.commands), - args: toPrismaJson(it.config.args), - environment: toPrismaJson(it.config.environment), + ...this.containerMapper.dbConfigToCreateConfigStatement(it.config), secrets: differentNode ? null : toPrismaJson(it.config.secrets), - initContainers: toPrismaJson(it.config.initContainers), - logConfig: toPrismaJson(it.config.logConfig), - restartPolicy: it.config.restartPolicy, - networkMode: it.config.networkMode, - networks: toPrismaJson(it.config.networks), - deploymentStrategy: it.config.deploymentStrategy, - healthCheckConfig: toPrismaJson(it.config.healthCheckConfig), - resourceConfig: toPrismaJson(it.config.resourceConfig), - proxyHeaders: it.config.proxyHeaders ?? false, - useLoadBalancer: it.config.useLoadBalancer ?? false, - customHeaders: toPrismaJson(it.config.customHeaders), - extraLBAnnotations: toPrismaJson(it.config.extraLBAnnotations), - capabilities: toPrismaJson(it.config.capabilities), - annotations: toPrismaJson(it.config.annotations), - labels: toPrismaJson(it.config.labels), - dockerLabels: toPrismaJson(it.config.dockerLabels), - storageId: it.config.storageId, - storageConfig: toPrismaJson(it.config.storageConfig), }, - } - : undefined, + }, }, }), ), @@ -1110,105 +1166,83 @@ export default class DeployService { return message } - async getConfigBundleEnvironmentById(deploymentId: string): Promise { - const deployment = await this.prisma.deployment.findUniqueOrThrow({ - where: { - id: deploymentId, - }, - include: { - configBundles: { - include: { - configBundle: true, - }, - }, - }, - }) - - return this.getConfigBundleEnvironmentKeys(deployment.configBundles.map(it => it.configBundle)) - } - - private async transformImageEvent(event: ImageEvent): Promise { - const deployments = await this.prisma.deployment.findMany({ - select: { - id: true, - }, - where: { - versionId: event.versionId, - }, - }) - - return { - ...event, - deploymentIds: deployments.map(it => it.id), + private transformDomainEventToWsMessage(ev: DomainEvent): WsMessage | null { + switch (ev.type) { + case DEPLOYMENT_EVENT_INSTACE_CREATE: + return { + type: WS_TYPE_INSTANCES_ADDED, + data: this.mapper.instancesCreatedEventToMessage(ev.event as InstancesCreatedEvent), + } + case DEPLOYMENT_EVENT_INSTACE_DELETE: + return { + type: WS_TYPE_INSTANCE_DELETED, + data: this.mapper.instanceDeletedEventToMessage(ev.event as InstanceDeletedEvent), + } + case DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE: + return { + type: WS_TYPE_DEPLOYMENT_BUNDLES_UPDATED, + data: this.mapper.bundlesUpdatedEventToMessage(ev.event as DeploymentConfigBundlesUpdatedEvent), + } + default: { + this.logger.error(`Unhandled domain event ${ev.type}`) + return null + } } } - private async onImageAddedToVersion(event: DeploymentImageEvent): Promise { - const versionId = event.images?.length > 0 ? event.versionId : null - const deployments = await this.prisma.deployment.findMany({ - select: { - id: true, - }, - where: { - versionId, - }, - }) - - const instances = await Promise.all( - deployments.flatMap(deployment => - event.images.map(it => - this.prisma.instance.create({ - include: { - config: true, - image: { - include: { - config: true, - registry: true, - }, - }, - }, - data: { - deploymentId: deployment.id, - imageId: it.id, - }, - }), - ), + private async updateInvalidSecretsAndThrow(secrets: InvalidSecrets[]) { + await this.prisma.$transaction( + secrets.map(it => + this.prisma.containerConfig.update({ + where: { + id: it.configId, + }, + data: { + secrets: it.secrets, + }, + }), ), ) - return { - ...event, - instances, - } - } - - private mergeEnvironments(deployment: UniqueKeyValue[], configBundles: ConfigBundle[]): UniqueKeyValue[] { - const mergedEnvironment: Record = {} - - configBundles.forEach(bundle => { - const bundleEnv = (bundle.data as UniqueKeyValue[]) ?? [] - bundleEnv.forEach(it => { - mergedEnvironment[it.key] = it - }) - }) - - deployment.forEach(it => { - mergedEnvironment[it.key] = it + throw new CruxPreconditionFailedException({ + message: 'Some secrets are invalid', + property: 'secrets', + value: secrets.map(it => ({ ...it, secrets: undefined })), }) - - return Object.values(mergedEnvironment) } - private getConfigBundleEnvironmentKeys(configBundles: ConfigBundle[]): EnvironmentToConfigBundleNameMap { - const envToBundle: EnvironmentToConfigBundleNameMap = {} - - configBundles.forEach(bundle => { - const bundleEnv = (bundle.data as UniqueKeyValue[]) ?? [] - bundleEnv.forEach(it => { - envToBundle[it.key] = bundle.name - }) + private async collectLatestSuccessfulDeploymentsForPrefix( + nodeId: string, + prefix: string, + ): Promise { + const versions = await this.prisma.version.findMany({ + where: { + deployments: { + some: { + prefix, + nodeId, + status: 'successful', + }, + }, + }, + include: { + deployments: { + where: { + prefix, + nodeId, + status: 'successful', + }, + take: 1, + orderBy: { + createdAt: 'desc', + }, + include: { + config: true, + }, + }, + }, }) - return envToBundle + return versions.flatMap(it => it.deployments) } } diff --git a/web/crux/src/app/deploy/deploy.ws.gateway.ts b/web/crux/src/app/deploy/deploy.ws.gateway.ts index 1173d47c87..1296f4e3b7 100644 --- a/web/crux/src/app/deploy/deploy.ws.gateway.ts +++ b/web/crux/src/app/deploy/deploy.ws.gateway.ts @@ -1,6 +1,6 @@ import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' import { Identity } from '@ory/kratos-client' -import { Observable, Subject, map, of, startWith, takeUntil } from 'rxjs' +import { Observable, map, of, startWith, takeUntil } from 'rxjs' import { AuditLogLevel } from 'src/decorators/audit-logger.decorator' import { WsAuthorize, WsClient, WsMessage, WsSubscribe, WsSubscription, WsUnsubscribe } from 'src/websockets/common' import SocketClient from 'src/websockets/decorators/ws.client.decorator' @@ -27,32 +27,11 @@ import { } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' import { IdentityFromSocket } from '../token/jwt-auth.guard' -import { ImageDeletedMessage, WS_TYPE_IMAGE_DELETED } from '../version/version.message' -import { PatchDeploymentDto, PatchInstanceDto } from './deploy.dto' import { - DeploymentEnvUpdatedMessage, DeploymentEventListMessage, DeploymentEventMessage, - GetInstanceMessage, - GetInstanceSecretsMessage, - InstanceMessage, - InstanceSecretsMessage, - InstanceUpdatedMessage, - InstancesAddedMessage, - PatchDeploymentEnvMessage, - PatchInstanceMessage, - WS_TYPE_DEPLOYMENT_ENV_UPDATED, WS_TYPE_DEPLOYMENT_EVENT_LIST, WS_TYPE_FETCH_DEPLOYMENT_EVENTS, - WS_TYPE_GET_INSTANCE, - WS_TYPE_GET_INSTANCE_SECRETS, - WS_TYPE_INSTANCE, - WS_TYPE_INSTANCES_ADDED, - WS_TYPE_INSTANCE_SECRETS, - WS_TYPE_INSTANCE_UPDATED, - WS_TYPE_PATCH_DEPLOYMENT_ENV, - WS_TYPE_PATCH_INSTANCE, - WS_TYPE_PATCH_RECEIVED, } from './deploy.message' import DeployService from './deploy.service' @@ -66,8 +45,6 @@ const DeploymentId = () => WsParam('deploymentId') @UseGlobalWsGuards() @UseGlobalWsInterceptors() export default class DeployWebSocketGateway { - private deploymentEventCompleters = new Map>() - constructor( private readonly service: DeployService, private readonly editorServices: EditorServiceProvider, @@ -95,33 +72,10 @@ export default class DeployWebSocketGateway { data: me, }) - const key = `${client.token}-${deploymentId}` - if (this.deploymentEventCompleters.has(key)) { - this.deploymentEventCompleters.get(key).next(undefined) - this.deploymentEventCompleters.delete(key) - } - - const completer = new Subject() - this.deploymentEventCompleters.set(key, completer) - this.service - .subscribeToDeploymentEditEvents(deploymentId) - .pipe(takeUntil(completer)) - .subscribe(event => { - if (event.type === 'create' && event.instances) { - subscription.sendToAll({ - type: WS_TYPE_INSTANCES_ADDED, - data: event.instances.filter(it => it.deploymentId === deploymentId), - } as WsMessage) - } else if (event.type === 'delete') { - subscription.sendToAll({ - type: WS_TYPE_IMAGE_DELETED, - data: { - imageId: event.imageId, - }, - } as WsMessage) - } - }) + .subscribeToDomainEvents(deploymentId) + .pipe(takeUntil(subscription.getCompleter(client.token))) + .subscribe(message => subscription.sendToAll(message)) return { type: WS_TYPE_EDITOR_INIT, @@ -139,17 +93,12 @@ export default class DeployWebSocketGateway { @SocketSubscription() subscription: WsSubscription, ): Promise { const data = await this.service.onEditorLeft(deploymentId, client.token) + const message: WsMessage = { type: WS_TYPE_EDITOR_LEFT, data, } subscription.sendToAllExcept(client, message) - - const key = `${client.token}-${deploymentId}` - if (this.deploymentEventCompleters.has(key)) { - this.deploymentEventCompleters.get(key).next(undefined) - this.deploymentEventCompleters.delete(key) - } } @AuditLogLevel('disabled') @@ -184,108 +133,6 @@ export default class DeployWebSocketGateway { ) } - @SubscribeMessage(WS_TYPE_PATCH_INSTANCE) - async patchInstance( - @DeploymentId() deploymentId: string, - @SocketMessage() message: PatchInstanceMessage, - @IdentityFromSocket() identity: Identity, - @SocketClient() client: WsClient, - @SocketSubscription() subscription: WsSubscription, - ): Promise> { - const cruxReq: Pick = { - config: {}, - } - - if (message.resetSection) { - cruxReq.config[message.resetSection as string] = null - } else { - cruxReq.config = message.config - } - - await this.service.patchInstance(deploymentId, message.instanceId, cruxReq, identity) - - const updateMessage: WsMessage = { - type: WS_TYPE_INSTANCE_UPDATED, - data: { - instanceId: message.instanceId, - ...cruxReq.config, - }, - } - - subscription.sendToAllExcept(client, updateMessage) - - return { - type: WS_TYPE_PATCH_RECEIVED, - data: null, - } - } - - @SubscribeMessage(WS_TYPE_PATCH_DEPLOYMENT_ENV) - async patchDeploymentEnvironment( - @DeploymentId() deploymentId: string, - @SocketMessage() message: PatchDeploymentEnvMessage, - @SocketClient() client: WsClient, - @SocketSubscription() subscription: WsSubscription, - @IdentityFromSocket() identity: Identity, - ): Promise> { - const cruxReq: PatchDeploymentDto = { - environment: message.environment, - configBundleIds: message.configBundleIds, - } - - await this.service.patchDeployment(deploymentId, cruxReq, identity) - - const configBundleEnvironment = await this.service.getConfigBundleEnvironmentById(deploymentId) - - const response: WsMessage = { - type: WS_TYPE_DEPLOYMENT_ENV_UPDATED, - data: { - ...message, - configBundleEnvironment, - }, - } as WsMessage - - if (message.configBundleIds) { - // If config bundles change send the response to every client - // so the configBundleEnvironment will update - subscription.sendToAll(response) - } else { - subscription.sendToAllExcept(client, response) - } - - return { - type: WS_TYPE_PATCH_RECEIVED, - data: null, - } - } - - @AuditLogLevel('disabled') - @SubscribeMessage(WS_TYPE_GET_INSTANCE) - async getInstance(@SocketMessage() message: GetInstanceMessage): Promise> { - const instance = await this.service.getInstance(message.id) - - return { - type: WS_TYPE_INSTANCE, - data: instance, - } - } - - @AuditLogLevel('disabled') - @SubscribeMessage(WS_TYPE_GET_INSTANCE_SECRETS) - async getInstanceSecrets( - @SocketMessage() message: GetInstanceSecretsMessage, - ): Promise> { - const secrets = await this.service.getInstanceSecrets(message.id) - - return { - type: WS_TYPE_INSTANCE_SECRETS, - data: { - instanceId: message.id, - keys: secrets.keys ?? [], - }, - } - } - @AuditLogLevel('disabled') @SubscribeMessage(WS_TYPE_FOCUS_INPUT) async onFocusInput( diff --git a/web/crux/src/app/deploy/interceptors/deploy.delete.interceptor.ts b/web/crux/src/app/deploy/interceptors/deploy.delete.interceptor.ts index 315d04022a..ed21878b18 100644 --- a/web/crux/src/app/deploy/interceptors/deploy.delete.interceptor.ts +++ b/web/crux/src/app/deploy/interceptors/deploy.delete.interceptor.ts @@ -1,6 +1,6 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' import { Observable } from 'rxjs' -import { checkDeploymentDeletability } from 'src/domain/deployment' +import { deploymentIsDeletable } from 'src/domain/deployment' import { CruxPreconditionFailedException } from 'src/exception/crux-exception' import PrismaService from 'src/services/prisma.service' @@ -18,7 +18,7 @@ export default class DeleteDeploymentValidationInterceptor implements NestInterc }, }) - if (!checkDeploymentDeletability(deployment.status)) { + if (!deploymentIsDeletable(deployment.status)) { throw new CruxPreconditionFailedException({ message: 'Invalid deployment status.', property: 'status', diff --git a/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts b/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts index f674601eac..e96e894fce 100644 --- a/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts +++ b/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts @@ -1,10 +1,10 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' import { VersionTypeEnum } from '@prisma/client' import { Observable } from 'rxjs' -import { checkDeploymentMutability } from 'src/domain/deployment' +import { deploymentIsMutable } from 'src/domain/deployment' import { CruxConflictException, CruxPreconditionFailedException } from 'src/exception/crux-exception' import PrismaService from 'src/services/prisma.service' -import { PatchDeploymentDto } from '../deploy.dto' +import { UpdateDeploymentDto } from '../deploy.dto' @Injectable() export default class DeployPatchValidationInterceptor implements NestInterceptor { @@ -12,7 +12,7 @@ export default class DeployPatchValidationInterceptor implements NestInterceptor async intercept(context: ExecutionContext, next: CallHandler): Promise> { const req = context.switchToHttp().getRequest() - const body = req.body as PatchDeploymentDto + const body = req.body as UpdateDeploymentDto const deploymentId = req.params.deploymentId as string const deployment = await this.prisma.deployment.findUniqueOrThrow({ @@ -24,7 +24,7 @@ export default class DeployPatchValidationInterceptor implements NestInterceptor }, }) - if (!checkDeploymentMutability(deployment.status, deployment.version.type)) { + if (!deploymentIsMutable(deployment.status, deployment.version.type)) { throw new CruxPreconditionFailedException({ message: 'Invalid deployment status.', property: 'status', diff --git a/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts b/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts index 68862bb764..63cb4986d3 100644 --- a/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts +++ b/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts @@ -1,18 +1,14 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' import { Observable } from 'rxjs' import AgentService from 'src/app/agent/agent.service' -import ContainerMapper from 'src/app/container/container.mapper' import { ImageValidation } from 'src/app/image/image.dto' -import { - ContainerConfigData, - InstanceContainerConfigData, - UniqueKeyValue, - UniqueSecretKey, - UniqueSecretKeyValue, -} from 'src/domain/container' +import { ConcreteContainerConfigData, ContainerConfigData, ContainerConfigDataWithId } from 'src/domain/container' +import { getConflictsForConcreteConfig } from 'src/domain/container-conflict' +import { mergeConfigsWithConcreteConfig } from 'src/domain/container-merge' import { checkDeploymentDeployability } from 'src/domain/deployment' import { parseDyrectorioEnvRules } from 'src/domain/image' -import { createStartDeploymentSchema, yupValidate } from 'src/domain/validation' +import { missingSecretsOf } from 'src/domain/start-deployment' +import { createStartDeploymentSchema, nullifyUndefinedProperties, yupValidate } from 'src/domain/validation' import { CruxPreconditionFailedException } from 'src/exception/crux-exception' import PrismaService from 'src/services/prisma.service' import { StartDeploymentDto } from '../deploy.dto' @@ -22,7 +18,6 @@ export default class DeployStartValidationInterceptor implements NestInterceptor constructor( private prisma: PrismaService, private agentService: AgentService, - private containerMapper: ContainerMapper, ) {} async intercept(context: ExecutionContext, next: CallHandler): Promise> { @@ -34,9 +29,14 @@ export default class DeployStartValidationInterceptor implements NestInterceptor const deployment = await this.prisma.deployment.findUniqueOrThrow({ include: { version: true, + config: true, configBundles: { include: { - configBundle: true, + configBundle: { + include: { + config: true, + }, + }, }, }, instances: { @@ -62,13 +62,7 @@ export default class DeployStartValidationInterceptor implements NestInterceptor }, }) - if (deployment.instances.length < 1) { - throw new CruxPreconditionFailedException({ - message: 'There are no instances to deploy', - property: 'instances', - }) - } - + // deployment if (!checkDeploymentDeployability(deployment.status, deployment.version.type)) { throw new CruxPreconditionFailedException({ message: 'Invalid deployment status.', @@ -77,11 +71,19 @@ export default class DeployStartValidationInterceptor implements NestInterceptor }) } + // instances + if (deployment.instances.length < 1) { + throw new CruxPreconditionFailedException({ + message: 'There are no instances to deploy', + property: 'instances', + }) + } + const instances = deployment.instances.map(it => ({ ...it, - config: this.containerMapper.mergeConfigs( - it.image.config as any as ContainerConfigData, - (it.config ?? {}) as any as InstanceContainerConfigData, + config: mergeConfigsWithConcreteConfig( + [it.image.config as any as ContainerConfigData], + it.config as any as ConcreteContainerConfigData, ), })) @@ -100,103 +102,113 @@ export default class DeployStartValidationInterceptor implements NestInterceptor return prev }, {}) + nullifyUndefinedProperties(target.config) + target.configBundles.forEach(it => nullifyUndefinedProperties(it.configBundle.config)) + target.instances.forEach(instance => { + nullifyUndefinedProperties(instance.config) + nullifyUndefinedProperties(instance.image.config) + }) yupValidate(createStartDeploymentSchema(instanceValidations), target) - const node = this.agentService.getById(deployment.nodeId) - if (!node?.ready) { + const missingSecrets = deployment.instances + .map(it => { + const imageConfig = it.image.config as any as ContainerConfigData + const instanceConfig = it.config as any as ConcreteContainerConfigData + const mergedConfig = mergeConfigsWithConcreteConfig([imageConfig], instanceConfig) + + return missingSecretsOf(it.configId, mergedConfig) + }) + .filter(it => !!it) + + if (missingSecrets.length > 0) { throw new CruxPreconditionFailedException({ - message: 'Node is busy or unreachable', - property: 'nodeId', - value: deployment.nodeId, + message: 'Required secrets must have values!', + property: 'instanceSecrets', + value: missingSecrets, }) } - const missingSecrets = deployment.instances.filter(it => { - const imageSecrets = (it.image.config.secrets as UniqueSecretKey[]) ?? [] - const requiredSecrets = imageSecrets.filter(imageSecret => imageSecret.required).map(secret => secret.key) + // config bundles + if (deployment.configBundles.length > 0) { + const configs = deployment.configBundles.map(it => it.configBundle.config as any as ContainerConfigDataWithId) + const concreteConfig = deployment.config as any as ConcreteContainerConfigData + const conflicts = getConflictsForConcreteConfig(configs, concreteConfig) + if (conflicts) { + throw new CruxPreconditionFailedException({ + message: 'Unresolved conflicts between config bundles', + property: 'configBundles', + value: Object.keys(conflicts).join(', '), + }) + } - const instanceSecrets = (it.config?.secrets as UniqueSecretKeyValue[]) ?? [] - const hasSecrets = requiredSecrets.every(requiredSecret => { - const instanceSecret = instanceSecrets.find(secret => secret.key === requiredSecret) - if (!instanceSecret) { - return false - } + const mergedConfig = mergeConfigsWithConcreteConfig(configs, concreteConfig) + const missingInstanceSecrets = missingSecretsOf(deployment.configId, mergedConfig) + if (missingInstanceSecrets) { + throw new CruxPreconditionFailedException({ + message: 'Required secrets must have values!', + property: 'deploymentSecrets', + value: missingInstanceSecrets, + }) + } + } - return instanceSecret.encrypted && instanceSecret.value.length > 0 + // node + const node = this.agentService.getById(deployment.nodeId) + if (!node) { + throw new CruxPreconditionFailedException({ + message: 'Node is unreachable', + property: 'nodeId', + value: deployment.nodeId, }) + } - return !hasSecrets - }) - - if (missingSecrets.length > 0) { + if (!node.ready) { throw new CruxPreconditionFailedException({ - message: 'Required secrets must have values!', - property: 'instanceIds', - value: missingSecrets.map(it => ({ - id: it.id, - name: it.config?.name ?? it.image.name, - })), + message: 'Node is busy', + property: 'nodeId', + value: deployment.nodeId, }) } - if (!deployment.protected) { - const { - query: { ignoreProtected }, - } = req - - if (!ignoreProtected) { - const otherProtected = await this.prisma.deployment.findFirst({ - where: { - protected: true, - nodeId: deployment.nodeId, - prefix: deployment.prefix, - versionId: - deployment.version.type === 'incremental' - ? { - not: deployment.versionId, - } - : undefined, - }, - }) + // deployment protection + if (deployment.protected) { + // this is a protected deployment no need to check for protected prefixes - if (otherProtected) { - throw new CruxPreconditionFailedException({ - message: - deployment.version.type === 'incremental' - ? "There's a protected deployment with the same node and prefix in a different version" - : "There's a protected deployment with the same node and prefix", - property: 'protectedDeploymentId', - value: otherProtected.id, - }) - } - } + return next.handle() } - if (deployment.configBundles.length > 0) { - const deploymentEnv = (deployment.environment as UniqueKeyValue[]) ?? [] - const deploymentEnvKeys = deploymentEnv.map(it => it.key) - - const envToBundle: Record = {} // [Environment key]: config bundle name - - deployment.configBundles.forEach(it => { - const bundleEnv = (it.configBundle.data as UniqueKeyValue[]) ?? [] - - bundleEnv.forEach(env => { - if (deploymentEnvKeys.includes(env.key)) { - return - } - if (envToBundle[env.key]) { - throw new CruxPreconditionFailedException({ - message: `Environment variable ${env.key} in ${it.configBundle.name} is already defined by ${ - envToBundle[env.key] - }. Please define the key in the deployment or resolve the conflict in the bundles.`, - property: 'configBundleId', - value: it.configBundle.id, - }) - } - - envToBundle[env.key] = it.configBundle.name - }) + const { + query: { ignoreProtected }, + } = req + + if (ignoreProtected) { + // force deploy + + return next.handle() + } + + const otherProtected = await this.prisma.deployment.findFirst({ + where: { + protected: true, + nodeId: deployment.nodeId, + prefix: deployment.prefix, + versionId: + deployment.version.type === 'incremental' + ? { + not: deployment.versionId, + } + : undefined, + }, + }) + + if (otherProtected) { + throw new CruxPreconditionFailedException({ + message: + deployment.version.type === 'incremental' + ? "There's a protected deployment with the same node and prefix in a different version" + : "There's a protected deployment with the same node and prefix", + property: 'protectedDeploymentId', + value: otherProtected.id, }) } diff --git a/web/crux/src/app/image/image.dto.ts b/web/crux/src/app/image/image.dto.ts index b55cbf6288..970a520350 100644 --- a/web/crux/src/app/image/image.dto.ts +++ b/web/crux/src/app/image/image.dto.ts @@ -11,7 +11,7 @@ import { ValidateNested, } from 'class-validator' import { ENVIRONMENT_VALUE_TYPES, EnvironmentValueType } from 'src/domain/image' -import { ContainerConfigDto, PartialContainerConfigDto } from '../container/container.dto' +import { ContainerConfigDto } from '../container/container.dto' import { BasicRegistryDto } from '../registry/registry.dto' export class EnvironmentRule { @@ -46,9 +46,6 @@ export class ImageDto { @IsNumber() order: number - @ValidateNested() - config: ContainerConfigDto - @Type(() => Date) @IsDate() createdAt: Date @@ -60,6 +57,11 @@ export class ImageDto { labels: Record } +export class ImageDetailsDto extends ImageDto { + @ValidateNested() + config: ContainerConfigDto +} + export class AddImagesDto { @IsUUID() registryId: string @@ -75,5 +77,5 @@ export class PatchImageDto { @IsOptional() @ValidateNested() - config?: PartialContainerConfigDto | null + config?: ContainerConfigDto | null } diff --git a/web/crux/src/app/image/image.event.service.ts b/web/crux/src/app/image/image.event.service.ts deleted file mode 100644 index 0d6d45b9d5..0000000000 --- a/web/crux/src/app/image/image.event.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from '@nestjs/common' -import { Observable, Subject } from 'rxjs' -import { ImageDto } from './image.dto' -import { ImageEvent } from './image.event' - -@Injectable() -export default class ImageEventService { - private readonly events = new Subject() - - public imagesAddedToVersion(versionId: string, images: ImageDto[]) { - this.events.next({ - type: 'create', - versionId, - images, - }) - } - - public imageUpdated(versionId: string, image: ImageDto) { - this.events.next({ - type: 'update', - versionId, - images: [image], - }) - } - - public imageDeletedFromVersion(versionId: string, imageId: string) { - this.events.next({ - type: 'delete', - versionId, - imageId, - }) - } - - public watchEvents(): Observable { - return this.events.asObservable() - } -} diff --git a/web/crux/src/app/image/image.event.ts b/web/crux/src/app/image/image.event.ts deleted file mode 100644 index 1d29571f9b..0000000000 --- a/web/crux/src/app/image/image.event.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ImageDto } from './image.dto' - -const IMAGE_EVENT_TYPE_VALUES = ['create', 'update', 'delete'] as const -export type ImageEventType = (typeof IMAGE_EVENT_TYPE_VALUES)[number] - -export class ImageEvent { - type: ImageEventType - - versionId: string - - images?: ImageDto[] - - imageId?: string -} diff --git a/web/crux/src/app/image/image.http.controller.ts b/web/crux/src/app/image/image.http.controller.ts index 8b0d358017..91a02ff89f 100644 --- a/web/crux/src/app/image/image.http.controller.ts +++ b/web/crux/src/app/image/image.http.controller.ts @@ -30,7 +30,7 @@ import { IdentityFromRequest } from '../token/jwt-auth.guard' import ImageAddToVersionTeamAccessGuard from './guards/image.add-to-version.team-access.guard' import ImageOrderImagesTeamAccessGuard from './guards/image.order-images.team-access.guard' import ImageTeamAccessGuard from './guards/image.team-access.guard' -import { AddImagesDto, ImageDto, PatchImageDto } from './image.dto' +import { AddImagesDto, ImageDetailsDto, PatchImageDto } from './image.dto' import ImageService from './image.service' import ImageAddToVersionValidationInterceptor from './interceptors/image.add-images.interceptor' import DeleteImageValidationInterceptor from './interceptors/image.delete.interceptor' @@ -62,7 +62,7 @@ export default class ImageHttpController { summary: 'Fetch data of all images of a version.', }) @ApiOkResponse({ - type: ImageDto, + type: ImageDetailsDto, isArray: true, description: 'Data of images listed.', }) @@ -72,7 +72,7 @@ export default class ImageHttpController { @TeamSlug() _: string, @ProjectId() _projectId: string, @VersionId() versionId: string, - ): Promise { + ): Promise { return await this.service.getImagesByVersionId(versionId) } @@ -83,7 +83,7 @@ export default class ImageHttpController { "Fetch details of an image within a version. `projectId` refers to the project's ID, `versionId` refers to the version's ID, `imageId` refers to the image's ID. All, and `teamSlug` are required in the URL.

Image details consists `name`, `id`, `tag`, `order`, and the config of the image.", summary: 'Fetch data of an image of a version.', }) - @ApiOkResponse({ type: ImageDto, description: 'Data of an image.' }) + @ApiOkResponse({ type: ImageDetailsDto, description: 'Data of an image.' }) @ApiBadRequestResponse({ description: 'Bad request for image details.' }) @ApiForbiddenResponse({ description: 'Unauthorized request for image details.' }) @ApiNotFoundResponse({ description: 'Image not found.' }) @@ -93,7 +93,7 @@ export default class ImageHttpController { @ProjectId() _projectId: string, @VersionId() _versionId: string, @ImageId() imageId: string, - ): Promise { + ): Promise { return await this.service.getImageDetails(imageId) } @@ -106,7 +106,7 @@ export default class ImageHttpController { summary: 'Add images to a version.', }) @ApiBody({ type: AddImagesDto, isArray: true }) - @ApiCreatedResponse({ type: ImageDto, isArray: true, description: 'New image added.' }) + @ApiCreatedResponse({ type: ImageDetailsDto, isArray: true, description: 'New image added.' }) @ApiBadRequestResponse({ description: 'Bad request for images.' }) @ApiForbiddenResponse({ description: 'Unauthorized request for images.' }) @UseGuards(ImageAddToVersionTeamAccessGuard) @@ -118,7 +118,7 @@ export default class ImageHttpController { @VersionId() versionId: string, @Body(new ValidateNonEmptyArrayPipe()) request: AddImagesDto[], @IdentityFromRequest() identity: Identity, - ): Promise> { + ): Promise> { const images = await this.service.addImagesToVersion(teamSlug, versionId, request, identity) return { diff --git a/web/crux/src/app/image/image.mapper.ts b/web/crux/src/app/image/image.mapper.ts index 302a5684d0..9f41c5d1db 100644 --- a/web/crux/src/app/image/image.mapper.ts +++ b/web/crux/src/app/image/image.mapper.ts @@ -1,121 +1,62 @@ -import { Injectable } from '@nestjs/common' +import { forwardRef, Inject, Injectable } from '@nestjs/common' +import { DeploymentStrategy, ExposeStrategy, Image, NetworkMode, RestartPolicy } from '@prisma/client' +import { ContainerConfigData } from 'src/domain/container' +import { ImageDetails, ImageWithRegistry } from 'src/domain/image' import { - ContainerConfig, - DeploymentStrategy, - ExposeStrategy, - Image, - InstanceContainerConfig, - NetworkMode, - Registry, - RestartPolicy, -} from '@prisma/client' -import { - ContainerConfigData, - ContainerLogDriverType, - ContainerState, - ContainerVolumeType, - Volume, -} from 'src/domain/container' -import { toPrismaJson } from 'src/domain/utils' -import { Volume as ProtoVolume } from 'src/grpc/protobuf/proto/agent' -import { - DriverType, - ContainerState as ProtoContainerState, + networkModeToJSON, DeploymentStrategy as ProtoDeploymentStrategy, ExposeStrategy as ProtoExposeStrategy, NetworkMode as ProtoNetworkMode, RestartPolicy as ProtoRestartPolicy, - VolumeType as ProtoVolumeType, - driverTypeFromJSON, - networkModeFromJSON, - networkModeToJSON, - volumeTypeFromJSON, } from 'src/grpc/protobuf/proto/common' import ContainerMapper from '../container/container.mapper' import RegistryMapper from '../registry/registry.mapper' -import { ImageDto } from './image.dto' +import { ImageDetailsDto, ImageDto } from './image.dto' @Injectable() export default class ImageMapper { constructor( private registryMapper: RegistryMapper, + @Inject(forwardRef(() => ContainerMapper)) private readonly containerMapper: ContainerMapper, ) {} - toDto(it: ImageDetails): ImageDto { + toDto(it: ImageWithRegistry): ImageDto { return { id: it.id, name: it.name, tag: it.tag, order: it.order, registry: this.registryMapper.toDto(it.registry), - config: this.containerMapper.configDataToDto(it.config as any as ContainerConfigData), createdAt: it.createdAt, labels: it.labels as Record, } } - dbContainerConfigToCreateImageStatement( - config: ContainerConfig | InstanceContainerConfig, - ): Omit { + toDetailsDto(it: ImageDetails): ImageDetailsDto { return { - // common - name: config.name, - expose: config.expose, - routing: toPrismaJson(config.routing), - configContainer: toPrismaJson(config.configContainer), - // Set user to the given value, if not null or use 0 if specifically 0, otherwise set to default -1 - user: config.user ?? (config.user === 0 ? 0 : -1), - workingDirectory: config.workingDirectory, - tty: config.tty ?? false, - ports: toPrismaJson(config.ports), - portRanges: toPrismaJson(config.portRanges), - volumes: toPrismaJson(config.volumes), - commands: toPrismaJson(config.commands), - args: toPrismaJson(config.args), - environment: toPrismaJson(config.environment), - secrets: toPrismaJson(config.secrets), - initContainers: toPrismaJson(config.initContainers), - logConfig: toPrismaJson(config.logConfig), - storageSet: config.storageSet, - storageId: config.storageId, - storageConfig: toPrismaJson(config.storageConfig), - - // dagent - restartPolicy: config.restartPolicy, - networkMode: config.networkMode, - networks: toPrismaJson(config.networks), - dockerLabels: toPrismaJson(config.dockerLabels), - expectedState: toPrismaJson(config.expectedState), - - // crane - deploymentStrategy: config.deploymentStrategy, - healthCheckConfig: toPrismaJson(config.healthCheckConfig), - resourceConfig: toPrismaJson(config.resourceConfig), - proxyHeaders: config.proxyHeaders ?? false, - useLoadBalancer: config.useLoadBalancer ?? false, - customHeaders: toPrismaJson(config.customHeaders), - extraLBAnnotations: toPrismaJson(config.extraLBAnnotations), - capabilities: toPrismaJson(config.capabilities), - annotations: toPrismaJson(config.annotations), - labels: toPrismaJson(config.labels), - metrics: toPrismaJson(config.metrics), + id: it.id, + name: it.name, + tag: it.tag, + order: it.order, + registry: this.registryMapper.toDto(it.registry), + config: this.containerMapper.configDataToDto(it.config.id, 'image', it.config as any as ContainerConfigData), + createdAt: it.createdAt, + labels: it.labels as Record, } } - deploymentStrategyToProto(type: DeploymentStrategy): ProtoDeploymentStrategy { - if (!type) { - return null + dbImageToCreateImageStatement(image: Image): Omit { + const result = { + ...image, } - switch (type) { - case DeploymentStrategy.recreate: - return ProtoDeploymentStrategy.RECREATE - case DeploymentStrategy.rolling: - return ProtoDeploymentStrategy.ROLLING_UPDATE - default: - return ProtoDeploymentStrategy.DEPLOYMENT_STRATEGY_UNSPECIFIED - } + delete result.id + delete result.registryId + delete result.versionId + delete result.configId + + return result } deploymentStrategyToDb(type: ProtoDeploymentStrategy): DeploymentStrategy { @@ -133,21 +74,6 @@ export default class ImageMapper { } } - exposeStrategyToProto(type: ExposeStrategy): ProtoExposeStrategy { - if (!type) { - return null - } - - switch (type) { - case ExposeStrategy.expose: - return ProtoExposeStrategy.EXPOSE - case ExposeStrategy.exposeWithTls: - return ProtoExposeStrategy.EXPOSE_WITH_TLS - default: - return ProtoExposeStrategy.NONE_ES - } - } - exposeStrategyToDb(type: ProtoExposeStrategy): ExposeStrategy { if (!type) { return undefined @@ -163,25 +89,6 @@ export default class ImageMapper { } } - restartPolicyToProto(type: RestartPolicy): ProtoRestartPolicy { - if (!type) { - return null - } - - switch (type) { - case RestartPolicy.always: - return ProtoRestartPolicy.ALWAYS - case RestartPolicy.no: - return ProtoRestartPolicy.NO - case RestartPolicy.unlessStopped: - return ProtoRestartPolicy.UNLESS_STOPPED - case RestartPolicy.onFailure: - return ProtoRestartPolicy.ON_FAILURE - default: - return ProtoRestartPolicy.NO - } - } - restartPolicyToDb(type: ProtoRestartPolicy): RestartPolicy { if (!type) { return undefined @@ -201,14 +108,6 @@ export default class ImageMapper { } } - networkModeToProto(it: NetworkMode): ProtoNetworkMode { - if (!it) { - return null - } - - return networkModeFromJSON(it?.toUpperCase()) - } - networkModeToDb(it: ProtoNetworkMode): NetworkMode { if (!it) { return undefined @@ -220,55 +119,4 @@ export default class ImageMapper { return networkModeToJSON(it).toLowerCase() as NetworkMode } - - logDriverToProto(it: ContainerLogDriverType): DriverType { - switch (it) { - case undefined: - case null: - case 'none': - return DriverType.DRIVER_TYPE_NONE - case 'json-file': - return DriverType.JSON_FILE - default: - return driverTypeFromJSON(it.toUpperCase()) - } - } - - volumesToProto(volumes: Volume[]): ProtoVolume[] { - if (!volumes) { - return null - } - - return volumes.map(it => ({ ...it, type: this.volumeTypeToProto(it.type) })) - } - - volumeTypeToProto(it?: ContainerVolumeType): ProtoVolumeType { - if (!it) { - return ProtoVolumeType.RO - } - - return volumeTypeFromJSON(it.toUpperCase()) - } - - stateToProto(state: ContainerState): ProtoContainerState { - if (!state) { - return null - } - - switch (state) { - case 'running': - return ProtoContainerState.RUNNING - case 'waiting': - return ProtoContainerState.WAITING - case 'exited': - return ProtoContainerState.EXITED - default: - return ProtoContainerState.CONTAINER_STATE_UNSPECIFIED - } - } -} - -export type ImageDetails = Image & { - config: ContainerConfig - registry: Registry } diff --git a/web/crux/src/app/image/image.module.ts b/web/crux/src/app/image/image.module.ts index c054b5f9fc..209f911ff8 100644 --- a/web/crux/src/app/image/image.module.ts +++ b/web/crux/src/app/image/image.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' import EncryptionService from 'src/services/encryption.service' import KratosService from 'src/services/kratos.service' import PrismaService from 'src/services/prisma.service' @@ -9,14 +9,13 @@ import RegistryClientProvider from '../registry/registry-client.provider' import RegistryMapper from '../registry/registry.mapper' import RegistryModule from '../registry/registry.module' import TeamRepository from '../team/team.repository' -import ImageEventService from './image.event.service' import ImageHttpController from './image.http.controller' import ImageMapper from './image.mapper' import ImageService from './image.service' @Module({ - imports: [RegistryModule, EditorModule, ContainerModule, AuditLoggerModule], - exports: [ImageService, ImageMapper, ImageEventService], + imports: [RegistryModule, EditorModule, forwardRef(() => ContainerModule), AuditLoggerModule], + exports: [ImageService, ImageMapper], providers: [ PrismaService, ImageService, @@ -25,7 +24,6 @@ import ImageService from './image.service' RegistryMapper, KratosService, EncryptionService, - ImageEventService, RegistryClientProvider, ], controllers: [ImageHttpController], diff --git a/web/crux/src/app/image/image.service.ts b/web/crux/src/app/image/image.service.ts index 6d1aea3521..f658356a72 100644 --- a/web/crux/src/app/image/image.service.ts +++ b/web/crux/src/app/image/image.service.ts @@ -1,17 +1,15 @@ import { Injectable } from '@nestjs/common' +import { EventEmitter2 } from '@nestjs/event-emitter' import { Identity } from '@ory/kratos-client' -import { ContainerConfig } from '@prisma/client' -import { ContainerConfigData, UniqueKeyValue } from 'src/domain/container' -import { containerNameFromImageName } from 'src/domain/deployment' +import { UniqueKeyValue } from 'src/domain/container' +import { IMAGE_EVENT_ADD, IMAGE_EVENT_DELETE, ImageDeletedEvent, ImagesAddedEvent } from 'src/domain/domain-events' import { EnvironmentRule, parseDyrectorioEnvRules } from 'src/domain/image' import PrismaService from 'src/services/prisma.service' -import { v4 } from 'uuid' -import ContainerMapper from '../container/container.mapper' -import EditorServiceProvider from '../editor/editor.service.provider' +import { v4 as uuid } from 'uuid' +import ContainerConfigService from '../container/container-config.service' import RegistryClientProvider from '../registry/registry-client.provider' import TeamRepository from '../team/team.repository' -import { AddImagesDto, ImageDto, PatchImageDto } from './image.dto' -import ImageEventService from './image.event.service' +import { AddImagesDto, ImageDetailsDto, PatchImageDto } from './image.dto' import ImageMapper from './image.mapper' type LabelMap = Record @@ -21,16 +19,15 @@ type RegistryLabelMap = Record @Injectable() export default class ImageService { constructor( - private prisma: PrismaService, - private mapper: ImageMapper, - private containerMapper: ContainerMapper, - private editorServices: EditorServiceProvider, - private eventService: ImageEventService, + private readonly prisma: PrismaService, + private readonly mapper: ImageMapper, + private readonly containerConfigService: ContainerConfigService, + private readonly events: EventEmitter2, private readonly teamRepository: TeamRepository, private readonly registryClients: RegistryClientProvider, ) {} - async getImagesByVersionId(versionId: string): Promise { + async getImagesByVersionId(versionId: string): Promise { const images = await this.prisma.image.findMany({ where: { versionId, @@ -41,10 +38,10 @@ export default class ImageService { }, }) - return images.map(it => this.mapper.toDto(it)) + return images.map(it => this.mapper.toDetailsDto(it)) } - async getImageDetails(imageId: string): Promise { + async getImageDetails(imageId: string): Promise { const image = await this.prisma.image.findUniqueOrThrow({ where: { id: imageId, @@ -54,7 +51,7 @@ export default class ImageService { registry: true, }, }) - return this.mapper.toDto(image) + return this.mapper.toDetailsDto(image) } async addImagesToVersion( @@ -62,7 +59,7 @@ export default class ImageService { versionId: string, request: AddImagesDto[], identity: Identity, - ): Promise { + ): Promise { const teamId = await this.teamRepository.getTeamIdBySlug(teamSlug) const labelLookupPromises = request.map(async it => { @@ -123,24 +120,13 @@ export default class ImageService { registry: true, }, data: { - registryId: registyImages.registryId, - versionId, + registry: { connect: { id: registyImages.registryId } }, + version: { connect: { id: versionId } }, + config: { create: { type: 'image' } }, createdBy: identity.id, name: imageName, tag: imageTag, order: order++, - config: { - create: { - name: containerNameFromImageName(imageName), - deploymentStrategy: 'recreate', - expose: 'none', - networkMode: 'bridge', - proxyHeaders: false, - restartPolicy: 'no', - tty: false, - useLoadBalancer: false, - }, - }, }, }) @@ -161,7 +147,7 @@ export default class ImageService { const defaultEnvs = Object.entries(envRules) .filter(([, rule]) => rule.required || !!rule.default) .map(([key, rule]) => ({ - id: v4(), + id: uuid(), key, value: rule.default ?? '', })) @@ -183,35 +169,46 @@ export default class ImageService { ), ) - const dtos = images.map(it => this.mapper.toDto(it)) + const dtos = images.map(it => this.mapper.toDetailsDto(it)) - this.eventService.imagesAddedToVersion(versionId, dtos) + const event: ImagesAddedEvent = { + versionId, + images, + } + await this.events.emitAsync(IMAGE_EVENT_ADD, event) return dtos } async patchImage(teamSlug: string, imageId: string, request: PatchImageDto, identity: Identity): Promise { - const currentConfig = await this.prisma.containerConfig.findUniqueOrThrow({ + const currentConfig = await this.prisma.containerConfig.findFirstOrThrow({ where: { - imageId, + image: { + id: imageId, + }, }, }) - const configData = this.containerMapper.configDtoToConfigData( - currentConfig as any as ContainerConfigData, - request.config ?? {}, - ) - - let labels: Record = null + if (request.config) { + await this.containerConfigService.patchConfig( + currentConfig.id, + { + config: request.config, + }, + identity, + ) + } + let labels: Record if (request.tag) { - const image = await this.prisma.image.findFirst({ + const image = await this.prisma.image.findUniqueOrThrow({ where: { id: imageId, }, select: { name: true, registryId: true, + config: true, }, }) @@ -221,12 +218,13 @@ export default class ImageService { labels = await api.client.labels(image.name, request.tag) const rules = parseDyrectorioEnvRules(labels) - configData.environment = ImageService.mergeEnvironmentsRules(configData.environment, rules) + image.config.environment = ImageService.mergeEnvironmentsRules( + image.config.environment as UniqueKeyValue[], + rules, + ) } - const config: Omit = this.containerMapper.configDataToDb(configData) - - const image = await this.prisma.image.update({ + await this.prisma.image.update({ where: { id: imageId, }, @@ -237,17 +235,9 @@ export default class ImageService { data: { labels: labels ?? undefined, tag: request.tag ?? undefined, - config: { - update: { - data: config, - }, - }, updatedBy: identity.id, }, }) - - const dto = this.mapper.toDto(image) - this.eventService.imageUpdated(image.versionId, dto) } async deleteImage(imageId: string): Promise { @@ -257,13 +247,22 @@ export default class ImageService { }, select: { versionId: true, + instances: { + select: { + id: true, + configId: true, + deploymentId: true, + }, + }, }, }) - const editors = await this.editorServices.getService(image.versionId) - editors?.onDeleteItem(imageId) - - this.eventService.imageDeletedFromVersion(image.versionId, imageId) + const event: ImageDeletedEvent = { + versionId: image.versionId, + imageId, + instances: image.instances, + } + await this.events.emitAsync(IMAGE_EVENT_DELETE, event) } async orderImages(request: string[], identity: Identity): Promise { @@ -301,7 +300,7 @@ export default class ImageService { const [key, rule] = it map[key] = { - id: currentEnv[key]?.id ?? v4(), + id: currentEnv[key]?.id ?? uuid(), key, value: currentEnv[key]?.value ?? rule.default ?? '', } diff --git a/web/crux/src/app/node/node.dto.ts b/web/crux/src/app/node/node.dto.ts index 8dc8635b6c..9a395fe8d4 100755 --- a/web/crux/src/app/node/node.dto.ts +++ b/web/crux/src/app/node/node.dto.ts @@ -15,6 +15,7 @@ import { import { CONTAINER_STATE_VALUES, ContainerState } from 'src/domain/container' import { PaginatedList, PaginationQuery } from 'src/shared/dtos/paginating' import { ContainerIdentifierDto } from '../container/container.dto' +import { DEPLOYMENT_STATUS_VALUES, DeploymentStatusDto } from '../deploy/deploy.dto' export const NODE_SCRIPT_TYPE_VALUES = ['shell', 'powershell'] as const export type NodeScriptTypeDto = (typeof NODE_SCRIPT_TYPE_VALUES)[number] @@ -264,3 +265,16 @@ export class NodeContainerLogQuery { @Type(() => Number) take?: number } + +export class NodeDeploymentQueryDto extends PaginationQuery { + @IsOptional() + @IsString() + @Type(() => String) + @ApiProperty() + readonly filter?: string + + @IsOptional() + @ApiProperty({ enum: DEPLOYMENT_STATUS_VALUES }) + @IsIn(DEPLOYMENT_STATUS_VALUES) + readonly status?: DeploymentStatusDto +} diff --git a/web/crux/src/app/node/node.http.controller.ts b/web/crux/src/app/node/node.http.controller.ts index cb23483d09..d8aabcfe07 100644 --- a/web/crux/src/app/node/node.http.controller.ts +++ b/web/crux/src/app/node/node.http.controller.ts @@ -27,7 +27,7 @@ import { import { Identity } from '@ory/kratos-client' import UuidParams from 'src/decorators/api-params.decorator' import { CreatedResponse, CreatedWithLocation } from '../../interceptors/created-with-location.decorator' -import { DeploymentDto } from '../deploy/deploy.dto' +import { DeploymentListDto } from '../deploy/deploy.dto' import DeployService from '../deploy/deploy.service' import { DisableAuth, IdentityFromRequest } from '../token/jwt-auth.guard' import NodeTeamAccessGuard from './guards/node.team-access.http.guard' @@ -36,6 +36,7 @@ import { CreateNodeDto, NodeAuditLogListDto, NodeAuditLogQueryDto, + NodeDeploymentQueryDto, NodeDetailsDto, NodeDto, NodeGenerateScriptDto, @@ -265,13 +266,19 @@ export default class NodeHttpController { summary: 'Fetch the list of deployments.', }) @ApiOkResponse({ - type: DeploymentDto, - isArray: true, - description: 'List of deployments.', + type: DeploymentListDto, + description: 'Paginated list of deployments.', }) @ApiForbiddenResponse({ description: 'Unauthorized request for deployments.' }) - async getDeployments(@TeamSlug() teamSlug: string, @NodeId() nodeId: string): Promise { - return await this.deployService.getDeployments(teamSlug, nodeId) + async getDeployments( + @TeamSlug() teamSlug: string, + @NodeId() nodeId: string, + @Query() query: NodeDeploymentQueryDto, + ): Promise { + return await this.deployService.getDeployments(teamSlug, { + ...query, + nodeId, + }) } @Post(`${ROUTE_NODE_ID}/kick`) diff --git a/web/crux/src/app/node/node.service.spec.ts b/web/crux/src/app/node/node.service.spec.ts index dc1473d73b..ab4e15f6e1 100644 --- a/web/crux/src/app/node/node.service.spec.ts +++ b/web/crux/src/app/node/node.service.spec.ts @@ -96,9 +96,11 @@ describe('NodeService', () => { await nodeService.deleteContainer('test-node-id', 'test-prefix', 'test-name') expect(createAgentEventMock).toHaveBeenCalledWith('test-node-id', 'containerCommand', { - container: { - prefix: 'test-prefix', - name: 'test-name', + target: { + container: { + prefix: 'test-prefix', + name: 'test-name', + }, }, operation: 'deleteContainer', }) @@ -108,7 +110,9 @@ describe('NodeService', () => { await nodeService.deleteAllContainers('test-node-id', 'test-prefix') expect(createAgentEventMock).toHaveBeenCalledWith('test-node-id', 'containerCommand', { - prefix: 'test-prefix', + target: { + prefix: 'test-prefix', + }, operation: 'deleteContainers', }) }) diff --git a/web/crux/src/app/node/node.service.ts b/web/crux/src/app/node/node.service.ts index deb59627a6..de825a4d87 100644 --- a/web/crux/src/app/node/node.service.ts +++ b/web/crux/src/app/node/node.service.ts @@ -305,15 +305,19 @@ export default class NodeService { async deleteAllContainers(nodeId: string, prefix: string): Promise { await this.sendDeleteContainerCommand(nodeId, 'deleteContainers', { - prefix, + target: { + prefix, + }, }) } async deleteContainer(nodeId: string, prefix: string, name: string): Promise { await this.sendDeleteContainerCommand(nodeId, 'deleteContainer', { - container: { - prefix: prefix ?? '', - name, + target: { + container: { + prefix: prefix ?? '', + name, + }, }, }) } diff --git a/web/crux/src/app/node/pipes/node.get-script.pipe.ts b/web/crux/src/app/node/pipes/node.get-script.pipe.ts index a59dde79fd..e9f669fda4 100644 --- a/web/crux/src/app/node/pipes/node.get-script.pipe.ts +++ b/web/crux/src/app/node/pipes/node.get-script.pipe.ts @@ -27,7 +27,7 @@ export default class NodeGetScriptValidationPipe implements PipeTransform { } } - // throwing intentionally ambigous exceptions, so an attacker can not guess node ids + // throwing intentionally ambiguous exceptions, so an attacker can not guess node ids throw new CruxUnauthorizedException() } } diff --git a/web/crux/src/app/package/package.module.ts b/web/crux/src/app/package/package.module.ts index 550dff83a4..f1beea9d34 100644 --- a/web/crux/src/app/package/package.module.ts +++ b/web/crux/src/app/package/package.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common' import PrismaService from 'src/services/prisma.service' +import ContainerModule from '../container/container.module' import DeployModule from '../deploy/deploy.module' -import ImageModule from '../image/image.module' import NodeModule from '../node/node.module' import ProjectModule from '../project/project.module' import TeamModule from '../team/team.module' @@ -12,7 +12,7 @@ import PackageMapper from './package.mapper' import PackageService from './package.service' @Module({ - imports: [ProjectModule, VersionModule, TeamModule, NodeModule, ImageModule, DeployModule], + imports: [ProjectModule, VersionModule, TeamModule, NodeModule, ContainerModule, DeployModule], exports: [], controllers: [PackageHttpController], providers: [PrismaService, PackageService, PackageMapper, TeamRepository], diff --git a/web/crux/src/app/package/package.service.ts b/web/crux/src/app/package/package.service.ts index fa1bbf5f15..0118728b71 100644 --- a/web/crux/src/app/package/package.service.ts +++ b/web/crux/src/app/package/package.service.ts @@ -4,9 +4,9 @@ import { DeploymentStatusEnum } from '@prisma/client' import { VersionWithDeployments } from 'src/domain/version' import { ImageWithConfig, copyDeployment } from 'src/domain/version-increase' import PrismaService from 'src/services/prisma.service' +import ContainerMapper from '../container/container.mapper' import { DeploymentDto } from '../deploy/deploy.dto' import DeployMapper from '../deploy/deploy.mapper' -import ImageMapper from '../image/image.mapper' import TeamRepository from '../team/team.repository' import { CreatePackageDeploymentDto, @@ -26,7 +26,7 @@ class PackageService { constructor( private readonly mapper: PackageMapper, private readonly deployMapper: DeployMapper, - private readonly imageMapper: ImageMapper, + private readonly containerMapper: ContainerMapper, private readonly teamRepository: TeamRepository, private readonly prisma: PrismaService, ) {} @@ -129,7 +129,6 @@ class PackageService { })), }, updatedBy: identity.id, - updatedAt: new Date(), }, }) } @@ -213,7 +212,6 @@ class PackageService { id: packageId, }, data: { - updatedAt: new Date(), updatedBy: identity.id, }, }) @@ -232,7 +230,6 @@ class PackageService { id: packageId, }, data: { - updatedAt: new Date(), updatedBy: identity.id, environments: { update: { @@ -256,7 +253,6 @@ class PackageService { id: packageId, }, data: { - updatedAt: new Date(), updatedBy: identity.id, environments: { delete: { @@ -367,6 +363,7 @@ class PackageService { ], }, include: { + config: true, instances: { include: { config: true, @@ -380,32 +377,42 @@ class PackageService { if (sourceVersion.deployments.length < 1) { // create a new empty deployment - const deployment = await this.prisma.deployment.create({ - data: { - nodeId: env.nodeId, - prefix: env.prefix, - versionId: target.id, - status: DeploymentStatusEnum.preparing, - createdBy: identity.id, - instances: { - createMany: { - data: sourceVersion.images.map(it => ({ - imageId: it.id, - })), - }, + const deploy = await this.prisma.$transaction(async prisma => { + const deployment = await prisma.deployment.create({ + data: { + version: { connect: { id: target.id } }, + node: { connect: { id: env.nodeId } }, + config: { create: { type: 'deployment' } }, + prefix: env.prefix, + status: DeploymentStatusEnum.preparing, + createdBy: identity.id, }, - }, - include: { - node: true, - version: { - include: { - project: true, + include: { + node: true, + version: { + include: { + project: true, + }, }, }, - }, + }) + + await Promise.all( + sourceVersion.images.map(async image => { + await prisma.instance.create({ + data: { + deployment: { connect: { id: deployment.id } }, + image: { connect: { id: image.id } }, + config: { create: { type: 'instance' } }, + }, + }) + }), + ) + + return deployment }) - return this.deployMapper.toDto(deployment) + return this.deployMapper.toDto(deploy) } // copy deployment from target @@ -417,12 +424,27 @@ class PackageService { sourceVersion.deployments.at(0) const copiedDeployment = copyDeployment(sourceDeployment) + const data = this.deployMapper.dbDeploymentToCreateDeploymentStatement(copiedDeployment) const newDeployment = await this.prisma.deployment.create({ data: { - ...copiedDeployment, + ...data, createdBy: identity.id, - versionId: target.id, + version: { + connect: { + id: target.id, + }, + }, + node: { + connect: { + id: copiedDeployment.nodeId, + }, + }, + config: !copiedDeployment.config + ? undefined + : { + create: this.containerMapper.dbConfigToCreateConfigStatement(copiedDeployment.config), + }, instances: undefined, }, include: { @@ -479,8 +501,9 @@ class PackageService { if (!instance) { await this.prisma.instance.create({ data: { - deploymentId: newDeployment.id, - imageId: image.id, + deployment: { connect: { id: newDeployment.id } }, + image: { connect: { id: image.id } }, + config: { create: { type: 'instance' } }, }, }) } @@ -503,10 +526,8 @@ class PackageService { config: !instance.config ? undefined : { - create: this.imageMapper.dbContainerConfigToCreateImageStatement({ + create: this.containerMapper.dbConfigToCreateConfigStatement({ ...instance.config, - id: undefined, - instanceId: undefined, }), }, }, diff --git a/web/crux/src/app/pipeline/pipeline.service.ts b/web/crux/src/app/pipeline/pipeline.service.ts index e3486306cc..a6a166c15a 100644 --- a/web/crux/src/app/pipeline/pipeline.service.ts +++ b/web/crux/src/app/pipeline/pipeline.service.ts @@ -210,7 +210,6 @@ export default class PipelineService { ...this.mapper.detailsToDb(req, repo.projectId), hooks, token: !req.token ? undefined : this.encryptionService.encrypt(req.token), - updatedAt: new Date(), updatedBy: identity.id, }, }) @@ -316,7 +315,6 @@ export default class PipelineService { }, data: { ...this.mapper.eventWatcherToDb(req), - updatedAt: new Date(), updatedBy: identity.id, }, }) diff --git a/web/crux/src/app/project/project.mapper.ts b/web/crux/src/app/project/project.mapper.ts index 0995828941..d0fbc50816 100644 --- a/web/crux/src/app/project/project.mapper.ts +++ b/web/crux/src/app/project/project.mapper.ts @@ -1,8 +1,9 @@ import { Project } from '.prisma/client' import { Inject, Injectable, forwardRef } from '@nestjs/common' +import { VersionWithChildren } from 'src/domain/version' import { BasicProperties } from 'src/shared/dtos/shared.dto' import AuditMapper from '../audit/audit.mapper' -import VersionMapper, { VersionWithChildren } from '../version/version.mapper' +import VersionMapper from '../version/version.mapper' import { BasicProjectDto, ProjectDetailsDto, ProjectDto, ProjectListItemDto } from './project.dto' @Injectable() @@ -52,7 +53,7 @@ type ProjectWithVersions = Project & { deletable: boolean } -export type ProjectWithCount = Project & { +type ProjectWithCount = Project & { _count: { versions: number } diff --git a/web/crux/src/app/project/project.module.ts b/web/crux/src/app/project/project.module.ts index d81e37854d..0a149c6ba6 100644 --- a/web/crux/src/app/project/project.module.ts +++ b/web/crux/src/app/project/project.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' import KratosService from 'src/services/kratos.service' import PrismaService from 'src/services/prisma.service' import AuditLoggerModule from '../audit.logger/audit.logger.module' @@ -12,7 +12,7 @@ import ProjectMapper from './project.mapper' import ProjectService from './project.service' @Module({ - imports: [VersionModule, TeamModule, TokenModule, AuditLoggerModule], + imports: [forwardRef(() => VersionModule), TeamModule, TokenModule, AuditLoggerModule], exports: [ProjectMapper, ProjectService], controllers: [ProjectHttpController], providers: [PrismaService, ProjectService, ProjectMapper, TeamRepository, KratosService, AuditMapper], diff --git a/web/crux/src/app/storage/interceptors/storage.delete.interceptor.ts b/web/crux/src/app/storage/interceptors/storage.delete.interceptor.ts index 14616f79bf..b3d92b60f9 100644 --- a/web/crux/src/app/storage/interceptors/storage.delete.interceptor.ts +++ b/web/crux/src/app/storage/interceptors/storage.delete.interceptor.ts @@ -18,21 +18,8 @@ export default class StorageDeleteValidationInterceptor implements NestIntercept }, take: 1, }) - if (usedContainerConfig > 0) { - throw new CruxPreconditionFailedException({ - property: 'id', - value: storageId, - message: 'Storage is already in use.', - }) - } - const usedInstanceContainerConfig = await this.prisma.instanceContainerConfig.count({ - where: { - storageId, - }, - take: 1, - }) - if (usedInstanceContainerConfig > 0) { + if (usedContainerConfig > 0) { throw new CruxPreconditionFailedException({ property: 'id', value: storageId, diff --git a/web/crux/src/app/storage/storage.mapper.ts b/web/crux/src/app/storage/storage.mapper.ts index f76cf1f1a4..1d0381ec8f 100644 --- a/web/crux/src/app/storage/storage.mapper.ts +++ b/web/crux/src/app/storage/storage.mapper.ts @@ -23,7 +23,7 @@ export default class StorageMapper { return { ...this.listItemToDto(storage), public: !storage.accessKey, - inUse: storage._count.containerConfigs > 0 || storage._count.instanceConfigs > 0, + inUse: storage._count.containerConfigs > 0, } } @@ -47,6 +47,5 @@ export default class StorageMapper { type StorageWithCount = Storage & { _count: { containerConfigs: number - instanceConfigs: number } } diff --git a/web/crux/src/app/storage/storage.service.ts b/web/crux/src/app/storage/storage.service.ts index 8bb605b040..b32df2012e 100644 --- a/web/crux/src/app/storage/storage.service.ts +++ b/web/crux/src/app/storage/storage.service.ts @@ -32,8 +32,13 @@ export default class StorageService { include: { _count: { select: { - containerConfigs: true, - instanceConfigs: true, + containerConfigs: { + where: { + type: { + in: ['image', 'instance'], + }, + }, + }, }, }, }, @@ -60,7 +65,6 @@ export default class StorageService { ...storage, _count: { containerConfigs: 0, - instanceConfigs: 0, }, }) } diff --git a/web/crux/src/app/template/template.service.ts b/web/crux/src/app/template/template.service.ts index 70ddc1898e..b6bcdaa838 100644 --- a/web/crux/src/app/template/template.service.ts +++ b/web/crux/src/app/template/template.service.ts @@ -13,7 +13,7 @@ import { import PrismaService from 'src/services/prisma.service' import TemplateFileService, { TemplateContainerConfig, TemplateImage } from 'src/services/template.file.service' import { VERSIONLESS_PROJECT_VERSION_NAME } from 'src/shared/const' -import { v4 } from 'uuid' +import { v4 as uuid } from 'uuid' import ImageMapper from '../image/image.mapper' import { CreateProjectDto, ProjectDto } from '../project/project.dto' import ProjectService from '../project/project.service' @@ -95,18 +95,15 @@ export default class TemplateService { } private idify(object: T): T { - return { ...object, id: v4() } + return { ...object, id: uuid() } } - private mapTemplateConfig(config: TemplateContainerConfig): ContainerConfigData { - // TODO (polaroi8d): wait this until we'll rework the templates + private mapTemplateConfig(config: TemplateContainerConfig): Omit { + // TODO (polaroi8d): wait with this for the templates rework // TODO (@m8vago): validate containerConfigData return { ...config, - tty: config.tty ?? false, - useLoadBalancer: config.useLoadBalancer ?? false, - proxyHeaders: config.proxyHeaders ?? false, deploymentStrategy: config.deploymentStatregy ? this.imageMapper.deploymentStrategyToDb( deploymentStrategyFromJSON(config.deploymentStatregy.toLocaleUpperCase()), @@ -121,7 +118,7 @@ export default class TemplateService { expose: config.expose ? this.imageMapper.exposeStrategyToDb(exposeStrategyFromJSON(config.expose.toLocaleUpperCase())) : 'none', - networks: config.networks ? config.networks.map(it => ({ id: v4(), key: it })) : [], + networks: config.networks ? config.networks.map(it => ({ id: uuid(), key: it })) : [], ports: config.ports ? toPrismaJson(config.ports.map(it => this.idify(it))) : [], environment: config.environment ? config.environment.map(it => this.idify(it)) : [], args: config.args ? config.args.map(it => this.idify(it)) : [], @@ -196,7 +193,7 @@ export default class TemplateService { const images = templateImages.map((it, index) => { const registryId = registryLookup.find(reg => reg.name === it.registryName).id - const config: ContainerConfigData = this.mapTemplateConfig(it.config) + const config = this.mapTemplateConfig(it.config) return this.prisma.image.create({ include: { @@ -204,16 +201,25 @@ export default class TemplateService { registry: true, }, data: { - registryId, - versionId: version.id, createdBy: identity.id, name: it.image, order: index, tag: it.tag, + version: { connect: { id: version.id } }, + registry: { connect: { id: registryId } }, config: { create: { ...config, - id: undefined, + type: 'image', + updatedAt: undefined, + updatedBy: identity.id, + storage: !it.config.storageId + ? undefined + : { + connect: { + id: it.config.storageId, + }, + }, }, }, }, diff --git a/web/crux/src/app/version/version.domain-event.listener.ts b/web/crux/src/app/version/version.domain-event.listener.ts new file mode 100644 index 0000000000..7ac97f6e93 --- /dev/null +++ b/web/crux/src/app/version/version.domain-event.listener.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common' +import { OnEvent } from '@nestjs/event-emitter' +import { filter, Observable, Subject } from 'rxjs' +import { + IMAGE_EVENT_ADD, + IMAGE_EVENT_DELETE, + ImageDeletedEvent, + ImageEvent, + ImagesAddedEvent, +} from 'src/domain/domain-events' +import { DomainEvent } from 'src/shared/domain-event' + +@Injectable() +export default class VersionDomainEventListener { + private versionEvents = new Subject>() + + watchEvents(versionId: string): Observable> { + return this.versionEvents.pipe(filter(it => it.event.versionId === versionId)) + } + + @OnEvent(IMAGE_EVENT_ADD) + onImagesAdded(event: ImagesAddedEvent) { + const editEvent: DomainEvent = { + type: IMAGE_EVENT_ADD, + event, + } + + this.versionEvents.next(editEvent) + } + + @OnEvent(IMAGE_EVENT_DELETE) + onImagesDeleted(event: ImageDeletedEvent) { + const editEvent: DomainEvent = { + type: IMAGE_EVENT_DELETE, + event, + } + + this.versionEvents.next(editEvent) + } +} diff --git a/web/crux/src/app/version/version.dto.ts b/web/crux/src/app/version/version.dto.ts index d2aed6915b..06d4d8c5f3 100644 --- a/web/crux/src/app/version/version.dto.ts +++ b/web/crux/src/app/version/version.dto.ts @@ -3,7 +3,7 @@ import { Type } from 'class-transformer' import { IsBoolean, IsIn, IsNotEmpty, IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator' import { AuditDto } from '../audit/audit.dto' import { DeploymentWithBasicNodeDto } from '../deploy/deploy.dto' -import { ImageDto } from '../image/image.dto' +import { ImageDetailsDto } from '../image/image.dto' export const VERSION_TYPE_VALUES = ['incremental', 'rolling'] as const export type VersionTypeDto = (typeof VERSION_TYPE_VALUES)[number] @@ -88,8 +88,8 @@ export class VersionDetailsDto extends VersionDto { @IsOptional() autoCopyDeployments?: boolean - @Type(() => ImageDto) - images: ImageDto[] + @Type(() => ImageDetailsDto) + images: ImageDetailsDto[] deployments: DeploymentWithBasicNodeDto[] } diff --git a/web/crux/src/app/version/version.mapper.ts b/web/crux/src/app/version/version.mapper.ts index 1060134c6f..dd214b5a1b 100644 --- a/web/crux/src/app/version/version.mapper.ts +++ b/web/crux/src/app/version/version.mapper.ts @@ -1,21 +1,28 @@ import { Version } from '.prisma/client' -import { Inject, Injectable, forwardRef } from '@nestjs/common' -import { ProjectTypeEnum } from '@prisma/client' -import { versionIsDeletable, versionIsIncreasable, versionIsMutable } from 'src/domain/version' +import { forwardRef, Inject, Injectable } from '@nestjs/common' +import { ImageDeletedEvent, ImagesAddedEvent } from 'src/domain/domain-events' +import { + VersionDetails, + versionIsDeletable, + versionIsIncreasable, + versionIsMutable, + VersionWithChildren, +} from 'src/domain/version' import { VersionChainWithEdges } from 'src/domain/version-chain' import { BasicProperties } from '../../shared/dtos/shared.dto' import AuditMapper from '../audit/audit.mapper' -import { DeploymentWithNode } from '../deploy/deploy.dto' import DeployMapper from '../deploy/deploy.mapper' -import ImageMapper, { ImageDetails } from '../image/image.mapper' +import ImageMapper from '../image/image.mapper' import { NodeConnectionStatus } from '../node/node.dto' import { BasicVersionDto, VersionChainDto, VersionDetailsDto, VersionDto } from './version.dto' +import { ImageDeletedMessage, ImagesAddedMessage } from './version.message' @Injectable() export default class VersionMapper { constructor( @Inject(forwardRef(() => DeployMapper)) private deployMapper: DeployMapper, + @Inject(forwardRef(() => ImageMapper)) private imageMapper: ImageMapper, private auditMapper: AuditMapper, ) {} @@ -52,7 +59,7 @@ export default class VersionMapper { deletable: versionIsDeletable(version), increasable: versionIsIncreasable(version), autoCopyDeployments: version.autoCopyDeployments, - images: version.images.map(it => this.imageMapper.toDto(it)), + images: version.images.map(it => this.imageMapper.toDetailsDto(it)), deployments: version.deployments.map(it => this.deployMapper.toDeploymentWithBasicNodeDto(it, nodeStatusLookup.get(it.nodeId)), ), @@ -72,16 +79,16 @@ export default class VersionMapper { }, } } -} -export type VersionWithChildren = Version & { - children: { versionId: string }[] -} + imagesAddedEventToMessage(event: ImagesAddedEvent): ImagesAddedMessage { + return { + images: event.images.map(it => this.imageMapper.toDetailsDto(it)), + } + } -export type VersionDetails = VersionWithChildren & { - project: { - type: ProjectTypeEnum + imageDeletedEventToMessage(event: ImageDeletedEvent): ImageDeletedMessage { + return { + imageId: event.imageId, + } } - images: ImageDetails[] - deployments: DeploymentWithNode[] } diff --git a/web/crux/src/app/version/version.message.ts b/web/crux/src/app/version/version.message.ts index 933c22163a..9dd8d1b0b0 100644 --- a/web/crux/src/app/version/version.message.ts +++ b/web/crux/src/app/version/version.message.ts @@ -1,22 +1,31 @@ -import { ImageConfigProperty } from '../image/image.const' -import { AddImagesDto, ImageDto, PatchImageDto } from '../image/image.dto' +import { AddImagesDto, ImageDetailsDto } from '../image/image.dto' +export const WS_TYPE_GET_IMAGE = 'get-image' export type GetImageMessage = { id: string } export const WS_TYPE_IMAGE = 'image' -export type ImageMessage = ImageDto +export type ImageMessage = ImageDetailsDto +export const WS_TYPE_ADD_IMAGES = 'add-images' export type AddImagesMessage = { registryImages: AddImagesDto[] } +export const WS_TYPE_SET_IMAGE_TAG = 'set-image-tag' +export const WS_TYPE_IMAGE_TAG_UPDATED = 'image-tag-updated' +export type ImageTagMessage = { + imageId: string + tag: string +} + export const WS_TYPE_IMAGES_ADDED = 'images-added' export type ImagesAddedMessage = { - images: ImageDto[] + images: ImageDetailsDto[] } +export const WS_TYPE_DELETE_IMAGE = 'delete-image' export type DeleteImageMessage = { imageId: string } @@ -26,13 +35,6 @@ export type ImageDeletedMessage = { imageId: string } -export const WS_TYPE_IMAGE_UPDATED = 'image-updated' -export type PatchImageMessage = PatchImageDto & { - id: string - resetSection?: ImageConfigProperty -} - -export const WS_TYPE_PATCH_RECEIVED = 'patch-received' - export const WS_TYPE_IMAGES_WERE_REORDERED = 'images-were-reordered' +export const WS_TYPE_ORDER_IMAGES = 'order-images' export type OrderImagesMessage = string[] diff --git a/web/crux/src/app/version/version.module.ts b/web/crux/src/app/version/version.module.ts index 3b1345852d..8f1b814908 100644 --- a/web/crux/src/app/version/version.module.ts +++ b/web/crux/src/app/version/version.module.ts @@ -1,5 +1,5 @@ import { HttpModule } from '@nestjs/axios' -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' import NotificationTemplateBuilder from 'src/builders/notification.template.builder' import DomainNotificationService from 'src/services/domain.notification.service' import KratosService from 'src/services/kratos.service' @@ -7,23 +7,34 @@ import PrismaService from 'src/services/prisma.service' import AgentModule from '../agent/agent.module' import AuditLoggerModule from '../audit.logger/audit.logger.module' import AuditMapper from '../audit/audit.mapper' +import ContainerModule from '../container/container.module' import DeployModule from '../deploy/deploy.module' import EditorModule from '../editor/editor.module' import ImageModule from '../image/image.module' import TeamRepository from '../team/team.repository' import VersionChainHttpController from './version-chains.http.controller' +import VersionDomainEventListener from './version.domain-event.listener' import VersionHttpController from './version.http.controller' import VersionMapper from './version.mapper' import VersionService from './version.service' import VersionWebSocketGateway from './version.ws.gateway' @Module({ - imports: [ImageModule, HttpModule, DeployModule, AgentModule, EditorModule, AuditLoggerModule], + imports: [ + forwardRef(() => ImageModule), + forwardRef(() => ContainerModule), + HttpModule, + forwardRef(() => DeployModule), + forwardRef(() => AgentModule), + EditorModule, + AuditLoggerModule, + ], exports: [VersionService, VersionMapper], controllers: [VersionHttpController, VersionChainHttpController], providers: [ VersionService, VersionMapper, + VersionDomainEventListener, PrismaService, TeamRepository, NotificationTemplateBuilder, diff --git a/web/crux/src/app/version/version.service.ts b/web/crux/src/app/version/version.service.ts index f5d32e63e1..b1efb77a14 100644 --- a/web/crux/src/app/version/version.service.ts +++ b/web/crux/src/app/version/version.service.ts @@ -1,15 +1,22 @@ -import { Injectable, Logger } from '@nestjs/common' +import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common' import { Identity } from '@ory/kratos-client' import { DeploymentStatusEnum, Prisma } from '@prisma/client' +import { filter, map, Observable } from 'rxjs' +import { IMAGE_EVENT_ADD, IMAGE_EVENT_DELETE, ImageDeletedEvent, ImagesAddedEvent } from 'src/domain/domain-events' import { VersionMessage } from 'src/domain/notification-templates' import { versionChainMembersOf } from 'src/domain/version-chain' import { increaseIncrementalVersion } from 'src/domain/version-increase' import DomainNotificationService from 'src/services/domain.notification.service' import PrismaService from 'src/services/prisma.service' +import { DomainEvent } from 'src/shared/domain-event' +import { WsMessage } from 'src/websockets/common' import AgentService from '../agent/agent.service' +import ContainerMapper from '../container/container.mapper' +import DeployMapper from '../deploy/deploy.mapper' import { EditorLeftMessage, EditorMessage } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' import ImageMapper from '../image/image.mapper' +import VersionDomainEventListener from './version.domain-event.listener' import { CreateVersionDto, IncreaseVersionDto, @@ -20,17 +27,22 @@ import { VersionListQuery, } from './version.dto' import VersionMapper from './version.mapper' +import { WS_TYPE_IMAGE_DELETED, WS_TYPE_IMAGES_ADDED } from './version.message' @Injectable() export default class VersionService { private readonly logger = new Logger(VersionService.name) constructor( - private prisma: PrismaService, - private mapper: VersionMapper, - private imageMapper: ImageMapper, - private notificationService: DomainNotificationService, - private agentService: AgentService, + private readonly prisma: PrismaService, + private readonly mapper: VersionMapper, + private readonly imageMapper: ImageMapper, + private readonly deployMapper: DeployMapper, + @Inject(forwardRef(() => ContainerMapper)) + private readonly containerMapper: ContainerMapper, + private readonly domainEvents: VersionDomainEventListener, + private readonly notificationService: DomainNotificationService, + private readonly agentService: AgentService, private readonly editorServices: EditorServiceProvider, ) {} @@ -74,8 +86,15 @@ export default class VersionService { return versions > 0 } + subscribeToDomainEvents(versionId: string): Observable { + return this.domainEvents.watchEvents(versionId).pipe( + map(it => this.transformDomainEventToWsMessage(it)), + filter(it => !!it), + ) + } + async getVersionsByProjectId(projectId: string, user: Identity, query?: VersionListQuery): Promise { - const filter: Prisma.VersionWhereInput = { + const versionWhere: Prisma.VersionWhereInput = { name: query?.nameContains ? { contains: query.nameContains, @@ -92,7 +111,7 @@ export default class VersionService { project: { id: projectId, }, - ...filter, + ...versionWhere, }, }) @@ -192,6 +211,7 @@ export default class VersionService { }, deployments: { include: { + config: true, instances: { include: { config: true, @@ -248,20 +268,25 @@ export default class VersionService { if (defaultVersion) { const newImages = await Promise.all( defaultVersion.images.map(async image => { + const data = this.imageMapper.dbImageToCreateImageStatement(image) + const newImage = await prisma.image.create({ select: { id: true, }, data: { - ...image, - id: undefined, - versionId: newVersion.id, - config: { - create: { - ...this.imageMapper.dbContainerConfigToCreateImageStatement(image.config), - id: undefined, - }, - }, + ...data, + updatedAt: undefined, + updatedBy: undefined, + createdAt: undefined, + createdBy: identity.id, + registry: { connect: { id: image.registryId } }, + version: { connect: { id: newVersion.id } }, + config: !image.config + ? undefined + : { + create: this.containerMapper.dbConfigToCreateConfigStatement(image.config), + }, }, }) @@ -280,16 +305,32 @@ export default class VersionService { const deployments = await Promise.all( defaultVersion.deployments.map(async deployment => { + const data = this.deployMapper.dbDeploymentToCreateDeploymentStatement(deployment) + const newDeployment = await prisma.deployment.create({ select: { id: true, }, data: { - ...deployment, - id: undefined, + ...data, status: DeploymentStatusEnum.preparing, - versionId: newVersion.id, - environment: deployment.environment ?? [], + node: { + connect: { + id: deployment.nodeId, + }, + }, + version: { + connect: { + id: newVersion.id, + }, + }, + config: !deployment.config + ? undefined + : { + create: { + ...this.containerMapper.dbConfigToCreateConfigStatement(deployment.config), + }, + }, events: undefined, instances: undefined, protected: false, @@ -297,27 +338,32 @@ export default class VersionService { }) await Promise.all( - deployment.instances.map(it => - prisma.instance.create({ + deployment.instances.map(async it => { + await prisma.instance.create({ select: { id: true, }, data: { - ...it, - id: undefined, - deploymentId: newDeployment.id, - imageId: imageMap[it.imageId], - config: it.config - ? { + deployment: { + connect: { + id: newDeployment.id, + }, + }, + image: { + connect: { + id: imageMap[it.imageId], + }, + }, + config: !it.config + ? undefined + : { create: { - ...this.imageMapper.dbContainerConfigToCreateImageStatement(it.config), - id: undefined, + ...this.containerMapper.dbConfigToCreateConfigStatement(it.config), }, - } - : undefined, + }, }, - }), - ), + }) + }), ) }), ) @@ -432,6 +478,7 @@ export default class VersionService { ], }, include: { + config: true, instances: { include: { config: true, @@ -497,18 +544,19 @@ export default class VersionService { const { originalId } = image delete image.originalId + const data = this.imageMapper.dbImageToCreateImageStatement(image) + const createdImage = await prisma.image.create({ data: { - ...image, - versionId: version.id, + ...data, createdBy: identity.id, - config: { - create: this.imageMapper.dbContainerConfigToCreateImageStatement({ - ...image.config, - id: undefined, - imageId: undefined, - }), + registry: { + connect: { + id: image.registryId, + }, }, + version: { connect: { id: version.id } }, + config: { create: this.containerMapper.dbConfigToCreateConfigStatement(image.config) }, }, }) @@ -520,12 +568,28 @@ export default class VersionService { const imageIdMap = new Map(imageIdEntries) await Promise.all( increased.deployments.map(async deployment => { + const data = this.deployMapper.dbDeploymentToCreateDeploymentStatement(deployment) + const newDeployment = await prisma.deployment.create({ data: { - ...deployment, + ...data, createdBy: identity.id, - versionId: version.id, + node: { + connect: { + id: deployment.nodeId, + }, + }, + version: { + connect: { + id: version.id, + }, + }, instances: undefined, + config: { + create: { + type: 'deployment', + }, + }, }, }) @@ -552,11 +616,7 @@ export default class VersionService { config: !instance.config ? undefined : { - create: this.imageMapper.dbContainerConfigToCreateImageStatement({ - ...instance.config, - id: undefined, - instanceId: undefined, - }), + create: this.containerMapper.dbConfigToCreateConfigStatement(instance.config), }, }, }) @@ -612,4 +672,23 @@ export default class VersionService { return message } + + private transformDomainEventToWsMessage(ev: DomainEvent): WsMessage { + switch (ev.type) { + case IMAGE_EVENT_ADD: + return { + type: WS_TYPE_IMAGES_ADDED, + data: this.mapper.imagesAddedEventToMessage(ev.event as ImagesAddedEvent), + } + case IMAGE_EVENT_DELETE: + return { + type: WS_TYPE_IMAGE_DELETED, + data: this.mapper.imageDeletedEventToMessage(ev.event as ImageDeletedEvent), + } + default: { + this.logger.error(`Unhandled domain event ${ev.type}`) + return null + } + } + } } diff --git a/web/crux/src/app/version/version.ws.gateway.ts b/web/crux/src/app/version/version.ws.gateway.ts index bff0791f80..9af90dc479 100644 --- a/web/crux/src/app/version/version.ws.gateway.ts +++ b/web/crux/src/app/version/version.ws.gateway.ts @@ -1,5 +1,6 @@ import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' import { Identity } from '@ory/kratos-client' +import { takeUntil } from 'rxjs' import { AuditLogLevel } from 'src/decorators/audit-logger.decorator' import { WsAuthorize, WsClient, WsMessage, WsSubscribe, WsSubscription, WsUnsubscribe } from 'src/websockets/common' import SocketClient from 'src/websockets/decorators/ws.client.decorator' @@ -11,6 +12,7 @@ import { import WsParam from 'src/websockets/decorators/ws.param.decorator' import SocketMessage from 'src/websockets/decorators/ws.socket-message.decorator' import SocketSubscription from 'src/websockets/decorators/ws.subscription.decorator' +import { WS_TYPE_PATCH_RECEIVED } from '../container/container-config.message' import { EditorInitMessage, EditorLeftMessage, @@ -25,7 +27,6 @@ import { WS_TYPE_INPUT_FOCUSED, } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' -import { PatchImageDto } from '../image/image.dto' import ImageService from '../image/image.service' import { IdentityFromSocket } from '../token/jwt-auth.guard' import { @@ -34,24 +35,23 @@ import { GetImageMessage, ImageDeletedMessage, ImageMessage, - ImagesAddedMessage, + ImageTagMessage, OrderImagesMessage, - PatchImageMessage, + WS_TYPE_ADD_IMAGES, + WS_TYPE_DELETE_IMAGE, + WS_TYPE_GET_IMAGE, WS_TYPE_IMAGE, - WS_TYPE_IMAGES_ADDED, WS_TYPE_IMAGES_WERE_REORDERED, WS_TYPE_IMAGE_DELETED, - WS_TYPE_IMAGE_UPDATED, - WS_TYPE_PATCH_RECEIVED, + WS_TYPE_IMAGE_TAG_UPDATED, + WS_TYPE_ORDER_IMAGES, + WS_TYPE_SET_IMAGE_TAG, } from './version.message' import VersionService from './version.service' const VersionId = () => WsParam('versionId') const TeamSlug = () => WsParam('teamSlug') -// TODO(@m8vago): make an event aggregator for image updates patches etc -// so subscribers will be notified of the changes regardless of the transport platform - @WebSocketGateway({ namespace: ':teamSlug/projects/:projectId/versions/:versionId', }) @@ -87,6 +87,11 @@ export default class VersionWebSocketGateway { data: me, }) + this.service + .subscribeToDomainEvents(versionId) + .pipe(takeUntil(subscription.getCompleter(client.token))) + .subscribe(message => subscription.sendToAll(message)) + return { type: WS_TYPE_EDITOR_INIT, data: { @@ -111,7 +116,7 @@ export default class VersionWebSocketGateway { } @AuditLogLevel('disabled') - @SubscribeMessage('get-image') + @SubscribeMessage(WS_TYPE_GET_IMAGE) async getImage(@SocketMessage() message: GetImageMessage): Promise> { const data = await this.imageService.getImageDetails(message.id) @@ -121,27 +126,17 @@ export default class VersionWebSocketGateway { } as WsMessage } - @SubscribeMessage('add-images') + @SubscribeMessage(WS_TYPE_ADD_IMAGES) async addImages( @TeamSlug() teamSlug: string, @VersionId() versionId: string, @SocketMessage() message: AddImagesMessage, @IdentityFromSocket() identity: Identity, - @SocketSubscription() subscription: WsSubscription, ): Promise { - const images = await this.imageService.addImagesToVersion(teamSlug, versionId, message.registryImages, identity) - - const res: WsMessage = { - type: WS_TYPE_IMAGES_ADDED, - data: { - images, - }, - } - - subscription.sendToAll(res) + await this.imageService.addImagesToVersion(teamSlug, versionId, message.registryImages, identity) } - @SubscribeMessage('delete-image') + @SubscribeMessage(WS_TYPE_DELETE_IMAGE) async deleteImage( @SocketMessage() message: DeleteImageMessage, @SocketSubscription() subscription: WsSubscription, @@ -156,31 +151,26 @@ export default class VersionWebSocketGateway { subscription.sendToAll(res) } - @SubscribeMessage('patch-image') - async patchImage( + @SubscribeMessage(WS_TYPE_SET_IMAGE_TAG) + async setImageTag( @TeamSlug() teamSlug, @SocketClient() client: WsClient, - @SocketMessage() message: PatchImageMessage, + @SocketMessage() message: ImageTagMessage, @IdentityFromSocket() identity: Identity, @SocketSubscription() subscription: WsSubscription, ): Promise> { - let cruxReq: Pick = {} - - if (message.resetSection) { - cruxReq.config = {} - cruxReq.config[message.resetSection as string] = null - } else { - cruxReq = message - } - - await this.imageService.patchImage(teamSlug, message.id, cruxReq, identity) - - const res: WsMessage = { - type: WS_TYPE_IMAGE_UPDATED, - data: { - ...cruxReq, - id: message.id, + await this.imageService.patchImage( + teamSlug, + message.imageId, + { + tag: message.tag, }, + identity, + ) + + const res: WsMessage = { + type: WS_TYPE_IMAGE_TAG_UPDATED, + data: message, } subscription.sendToAllExcept(client, res) @@ -191,7 +181,7 @@ export default class VersionWebSocketGateway { } } - @SubscribeMessage('order-images') + @SubscribeMessage(WS_TYPE_ORDER_IMAGES) async orderImages( @SocketClient() client: WsClient, @SocketMessage() message: OrderImagesMessage, diff --git a/web/crux/src/domain/agent.spec.ts b/web/crux/src/domain/agent.spec.ts index ce9b7783f8..0dbe01be0d 100644 --- a/web/crux/src/domain/agent.spec.ts +++ b/web/crux/src/domain/agent.spec.ts @@ -200,7 +200,9 @@ describe('agent', () => { const commandChannel = firstValueFrom(agent.onConnected(jest.fn())) const deleteRequest: DeleteContainersRequest = { - prefix: 'prefix', + target: { + prefix: 'prefix', + }, } const deleteRes = agent.deleteContainers(deleteRequest) @@ -213,7 +215,7 @@ describe('agent', () => { agent.onCallback( 'deleteContainers', Agent.containerPrefixNameOf({ - prefix: deleteRequest.prefix, + prefix: deleteRequest.target.prefix, name: '', }), Empty, @@ -230,7 +232,9 @@ describe('agent', () => { const commandChannel = firstValueFrom(agent.onConnected(jest.fn())) const deleteRequest: DeleteContainersRequest = { - prefix: 'prefix', + target: { + prefix: 'prefix', + }, } const deleteRes = agent.deleteContainers(deleteRequest) @@ -306,9 +310,11 @@ describe('agent', () => { const commandChannel = firstValueFrom(agent.onConnected(jest.fn())) const req: ListSecretsRequest = { - container: { - prefix: 'prefix', - name: 'name', + target: { + container: { + prefix: 'prefix', + name: 'name', + }, }, } @@ -320,14 +326,17 @@ describe('agent', () => { }) const message: ListSecretsResponse = { - prefix: 'prefix', - name: 'name', + target: { + container: { + prefix: 'prefix', + name: 'name', + }, + }, publicKey: 'key', - hasKeys: true, keys: ['k1', 'k2', 'k3'], } - agent.onCallback('listSecrets', Agent.containerPrefixNameOf(req.container), message) + agent.onCallback('listSecrets', Agent.containerPrefixNameOf(req.target.container), message) const secretsActual = await secrets expect(secretsActual).toEqual(message) @@ -339,9 +348,11 @@ describe('agent', () => { const commandChannel = firstValueFrom(agent.onConnected(jest.fn())) const req: ListSecretsRequest = { - container: { - prefix: 'prefix', - name: 'name', + target: { + container: { + prefix: 'prefix', + name: 'name', + }, }, } diff --git a/web/crux/src/domain/agent.ts b/web/crux/src/domain/agent.ts index 76acb1883f..2792a1d694 100755 --- a/web/crux/src/domain/agent.ts +++ b/web/crux/src/domain/agent.ts @@ -22,6 +22,7 @@ import { ContainerIdentifier, ContainerInspectResponse, ContainerLogListResponse, + ContainerOrPrefix, DeleteContainersRequest, DeploymentStatusMessage, Empty, @@ -106,7 +107,7 @@ export class Agent { this.outdated = options.outdated const callbacks: Record> = { - listSecrets: (req: ListSecretsRequest) => [Agent.containerPrefixNameOf(req.container), { listSecrets: req }], + listSecrets: (req: ListSecretsRequest) => [Agent.containerPrefixNameOrPrefixOf(req.target), { listSecrets: req }], containerLog: (req: ContainerLogRequest) => [ Agent.containerPrefixNameOf(req.container), { @@ -121,12 +122,7 @@ export class Agent { { containerInspect: req }, ], deleteContainers: (req: DeleteContainersRequest) => [ - Agent.containerPrefixNameOf( - req?.container ?? { - prefix: req.prefix, - name: null, - }, - ), + Agent.containerPrefixNameOrPrefixOf(req.target), { deleteContainers: req }, ], } @@ -494,6 +490,9 @@ export class Agent { public static containerPrefixNameOf = (id: ContainerIdentifier): string => !id.prefix ? id.name : `${id.prefix}-${id.name ?? ''}` + + public static containerPrefixNameOrPrefixOf = (target: ContainerOrPrefix) => + this.containerPrefixNameOf(target.container ?? { prefix: target.prefix, name: '' }) } export type AgentConnectionMessage = { diff --git a/web/crux/src/domain/container-conflict.ts b/web/crux/src/domain/container-conflict.ts new file mode 100644 index 0000000000..039d232d06 --- /dev/null +++ b/web/crux/src/domain/container-conflict.ts @@ -0,0 +1,676 @@ +import { + ConcreteContainerConfigData, + ContainerConfigData, + ContainerConfigDataWithId, + ContainerPortRange, + Log, + Marker, + Port, + PortRange, + ResourceConfig, + UniqueKeyValue, + Volume, +} from './container' + +export type ConflictedUniqueItem = { + key: string + configIds: string[] +} + +export type ConflictedPort = { + internal: number + configIds: string[] +} + +export type ConflictedPortRange = { + range: PortRange + configIds: string[] +} + +export type ConflictedLog = { + driver?: string[] + options?: ConflictedUniqueItem[] +} + +export type ConflictedResoureConfig = { + limits?: string[] + requests?: string[] +} + +export type ConflictedMarker = { + service?: ConflictedUniqueItem[] + deployment?: ConflictedUniqueItem[] + ingress?: ConflictedUniqueItem[] +} + +type ConflictedLogKeys = { + driver?: boolean + options?: string[] +} + +type ConflictedResoureConfigKeys = Partial> +type ConflictedMarkerKeys = Partial> + +// config ids where the given property is present +export type ConflictedContainerConfigData = { + // common + name?: string[] + environment?: ConflictedUniqueItem[] + routing?: string[] + expose?: string[] + user?: string[] + workingDirectory?: string[] + tty?: string[] + configContainer?: string[] + ports?: ConflictedPort[] + portRanges?: ConflictedPortRange[] + volumes?: ConflictedUniqueItem[] + initContainers?: string[] + capabilities?: ConflictedUniqueItem[] + storage?: string[] + + // dagent + logConfig?: string[] + restartPolicy?: string[] + networkMode?: string[] + dockerLabels?: ConflictedUniqueItem[] + expectedState?: string[] + + // crane + deploymentStrategy?: string[] + proxyHeaders?: string[] + useLoadBalancer?: string[] + extraLBAnnotations?: ConflictedUniqueItem[] + healthCheckConfig?: string[] + resourceConfig?: string[] + annotations?: ConflictedMarker + labels?: ConflictedMarker + metrics?: string[] +} + +export const rangesOverlap = (one: PortRange, other: PortRange): boolean => one.from <= other.to && other.from <= one.to +export const rangesAreEqual = (one: PortRange, other: PortRange): boolean => + one.from === other.from && one.to === other.to + +const appendConflict = (conflicts: string[], oneId: string, otherId: string): string[] => { + if (!conflicts) { + return [oneId, otherId] + } + + if (!conflicts.includes(oneId)) { + conflicts.push(oneId) + } + + if (!conflicts.includes(otherId)) { + conflicts.push(otherId) + } + + return conflicts +} + +const appendUniqueItemConflicts = ( + conflicts: ConflictedUniqueItem[], + oneId: string, + otherId: string, + keys: string[], +): ConflictedUniqueItem[] => { + if (!conflicts) { + return keys.map(it => ({ + key: it, + configIds: [oneId, otherId], + })) + } + + keys.forEach(key => { + let conflict = conflicts.find(it => it.key === key) + if (!conflict) { + conflict = { + key, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendPortConflicts = ( + conflicts: ConflictedPort[], + oneId: string, + otherId: string, + internalPorts: number[], +): ConflictedPort[] => { + if (!conflicts) { + return internalPorts.map(it => ({ + internal: it, + configIds: [oneId, otherId], + })) + } + + internalPorts.forEach(internalPort => { + let conflict = conflicts.find(it => internalPort === it.internal) + if (!conflict) { + conflict = { + internal: internalPort, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendPortRangeConflicts = ( + conflicts: ConflictedPortRange[], + oneId: string, + otherId: string, + ranges: PortRange[], +): ConflictedPortRange[] => { + if (!conflicts) { + return ranges.map(it => ({ + range: it, + configIds: [oneId, otherId], + })) + } + + ranges.forEach(range => { + let conflict = conflicts.find(it => rangesAreEqual(it.range, range)) + if (!conflict) { + conflict = { + range, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendLogConflict = ( + conflicts: ConflictedLog, + oneId: string, + otherId: string, + keys: ConflictedLogKeys, +): ConflictedLog => { + if (!conflicts) { + conflicts = {} + } + + if (keys.driver) { + conflicts.driver = appendConflict(conflicts.driver, oneId, otherId) + } + + if (keys.options) { + conflicts.options = appendUniqueItemConflicts(conflicts.options, oneId, otherId, keys.options) + } + + return conflicts +} + +const appendResourceConfigConflict = ( + conflicts: ConflictedResoureConfig, + oneId: string, + otherId: string, + keys: ConflictedResoureConfigKeys, +): ConflictedResoureConfig => { + if (!conflicts) { + conflicts = {} + } + + if (keys.limits) { + conflicts.limits = appendConflict(conflicts.limits, oneId, otherId) + } + + if (keys.requests) { + conflicts.requests = appendConflict(conflicts.requests, oneId, otherId) + } + + return conflicts +} + +const appendMarkerConflict = ( + conflicts: ConflictedMarker, + oneId: string, + otherId: string, + keys: ConflictedMarkerKeys, +): ConflictedMarker => { + if (!conflicts) { + conflicts = {} + } + + if (keys.deployment) { + conflicts.deployment = appendUniqueItemConflicts(conflicts.deployment, oneId, otherId, keys.deployment) + } + + if (keys.ingress) { + conflicts.ingress = appendUniqueItemConflicts(conflicts.ingress, oneId, otherId, keys.ingress) + } + + if (keys.service) { + conflicts.service = appendUniqueItemConflicts(conflicts.service, oneId, otherId, keys.service) + } + + return conflicts +} + +const stringsConflict = (one: string, other: string): boolean => { + if (typeof one !== 'string' || typeof other !== 'string') { + // one of them are null or uninterpretable + return false + } + + return one !== other +} + +const booleansConflict = (one: boolean, other: boolean): boolean => { + if (typeof one !== 'boolean' || typeof other !== 'boolean') { + // one of them are null or uninterpretable + return false + } + + return one !== other +} + +const numbersConflict = (one: number, other: number): boolean => { + if (typeof one !== 'number' || typeof other !== 'number') { + // some of them are null or uninterpretable + return false + } + + return one !== other +} + +const objectsConflict = (one: object, other: object): boolean => { + if (typeof one !== 'object' || typeof other !== 'object') { + // some of them are null or uninterpretable + return false + } + + return JSON.stringify(one) !== JSON.stringify(other) +} + +// returns the conflicting keys +const uniqueKeyValuesConflict = (one: UniqueKeyValue[], other: UniqueKeyValue[]): string[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => { + const otherItem = other.find(it => it.key === item.key) + if (!otherItem) { + return false + } + + return item.value !== otherItem.value + }) + .map(it => it.key) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting internal ports +const portsConflict = (one: Port[], other: Port[]): number[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one.filter(item => other.find(it => it.internal === item.internal)).map(it => it.internal) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting internal ranges +const portRangesConflict = (one: ContainerPortRange[], other: ContainerPortRange[]): PortRange[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => + other.find(it => rangesOverlap(item.internal, it.internal) || rangesOverlap(item.external, item.internal)), + ) + .map(it => it.internal) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting paths +const volumesConflict = (one: Volume[], other: Volume[]): string[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => { + const otherItem = other.find(it => it.path === item.path) + + return objectsConflict(item, otherItem) + }) + .map(it => it.path) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +const logsConflict = (one: Log, other: Log): ConflictedLogKeys | null => { + if (!one || !other) { + return null + } + + const driver = stringsConflict(one.driver, other.driver) + const options = uniqueKeyValuesConflict(one.options, other.options) + + const conflicts: ConflictedLogKeys = {} + + if (driver) { + conflicts.driver = driver + } + + if (options) { + conflicts.options = options + } + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const resoureConfigsConflict = (one: ResourceConfig, other: ResourceConfig): ConflictedResoureConfigKeys | null => { + if (!one || !other) { + return null + } + + const conflicts: ConflictedResoureConfigKeys = { + limits: objectsConflict(one.limits, other.limits), + requests: objectsConflict(one.requests, other.requests), + } + + if (!Object.values(conflicts).find(it => it)) { + // no conflicts + return null + } + + return conflicts +} + +const markersConflict = (one: Marker, other: Marker): ConflictedMarkerKeys | null => { + if (!one || !other) { + return null + } + + const deployment = uniqueKeyValuesConflict(one.deployment, other.deployment) + const ingress = uniqueKeyValuesConflict(one.ingress, other.ingress) + const service = uniqueKeyValuesConflict(one.service, other.service) + + const conflicts: ConflictedMarkerKeys = {} + + if (deployment) { + conflicts.deployment = deployment + } + + if (ingress) { + conflicts.ingress = ingress + } + + if (service) { + conflicts.service = service + } + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const collectConflicts = ( + conflicts: ConflictedContainerConfigData, + one: ContainerConfigDataWithId, + other: ContainerConfigDataWithId, +) => { + const checkStringConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as string + const otherValue = other[key] as string + + if (stringsConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkUniqueKeyValuesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as UniqueKeyValue[] + const otherValue = other[key] as UniqueKeyValue[] + + const uniqueKeyValueConflicts = uniqueKeyValuesConflict(oneValue, otherValue) + if (uniqueKeyValueConflicts) { + conflicts[key] = appendUniqueItemConflicts(conflicts[key], one.id, other.id, uniqueKeyValueConflicts) + } + } + + const checkBooleanConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as boolean + const otherValue = other[key] as boolean + + if (booleansConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkNumberConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as number + const otherValue = other[key] as number + + if (numbersConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkObjectConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as object + const otherValue = other[key] as object + + if (objectsConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkPortsConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Port[] + const otherValue = other[key] as Port[] + + const portsConflicts = portsConflict(oneValue, otherValue) + if (portsConflicts) { + conflicts[key] = appendPortConflicts(conflicts[key], one.id, other.id, portsConflicts) + } + } + + const checkPortRangesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as ContainerPortRange[] + const otherValue = other[key] as ContainerPortRange[] + + const portRangesConflicts = portRangesConflict(oneValue, otherValue) + if (portRangesConflicts) { + conflicts[key] = appendPortRangeConflicts(conflicts[key], one.id, other.id, portRangesConflicts) + } + } + + const checkVolumesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Volume[] + const otherValue = other[key] as Volume[] + + const volumeConflicts = volumesConflict(oneValue, otherValue) + if (volumeConflicts) { + conflicts[key] = appendUniqueItemConflicts(conflicts[key], one.id, other.id, volumeConflicts) + } + } + + const checkStorageConflict = () => { + if (objectsConflict(one, other)) { + conflicts.storage = appendConflict(conflicts.storage, one.id, other.id) + } + } + + const checkLogsConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Log + const otherValue = other[key] as Log + + const logConflicts = logsConflict(oneValue, otherValue) + if (logConflicts) { + conflicts[key] = appendLogConflict(conflicts[key], one.id, other.id, logConflicts) + } + } + + const checkResourceConfigConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as ResourceConfig + const otherValue = other[key] as ResourceConfig + + const resourceConfigConflicts = resoureConfigsConflict(oneValue, otherValue) + if (resourceConfigConflicts) { + conflicts[key] = appendResourceConfigConflict(conflicts[key], one.id, other.id, resourceConfigConflicts) + } + } + + const checkMarkerConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Marker + const otherValue = other[key] as Marker + + const markerConflicts = markersConflict(oneValue, otherValue) + if (markerConflicts) { + conflicts[key] = appendMarkerConflict(conflicts[key], one.id, other.id, markerConflicts) + } + } + + // common + checkStringConflict('name') + checkUniqueKeyValuesConflict('environment') + // 'secrets' are keys only so duplicates are allowed + checkObjectConflict('routing') + checkStringConflict('expose') + checkNumberConflict('user') + checkStringConflict('workingDirectory') + checkBooleanConflict('tty') + checkObjectConflict('configContainer') + checkPortsConflict('ports') + checkPortRangesConflict('portRanges') + checkVolumesConflict('volumes') + // 'commands' are keys only so duplicates are allowed + // 'args' are keys only so duplicates are allowed + checkObjectConflict('initContainers') // TODO (@m8vago) compare them correctly after the init container rework + checkUniqueKeyValuesConflict('capabilities') + checkStorageConflict() + + // dagent + checkLogsConflict('logConfig') + checkStringConflict('restartPolicy') + checkStringConflict('networkMode') + // 'networks' are keys only so duplicates are allowed + checkUniqueKeyValuesConflict('dockerLabels') + checkStringConflict('expectedState') + + // crane + checkStringConflict('deploymentStrategy') + // 'customHeaders' are keys only so duplicates are allowed + checkBooleanConflict('proxyHeaders') + checkBooleanConflict('useLoadBalancer') + checkUniqueKeyValuesConflict('extraLBAnnotations') + checkObjectConflict('healthCheckConfig') + checkResourceConfigConflict('resourceConfig') + checkMarkerConflict('annotations') + checkMarkerConflict('labels') + checkObjectConflict('metrics') + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +type ContainerConfigDataProperty = keyof ContainerConfigData +export const checkForConflicts = ( + configs: ContainerConfigDataWithId[], + definedKeys: ContainerConfigDataProperty[] = [], +): ConflictedContainerConfigData | null => { + configs = configs.map(conf => { + const newConf: ContainerConfigDataWithId = { + ...conf, + } + + Object.keys(conf).forEach(it => { + const prop = it as ContainerConfigDataProperty + if (!definedKeys.includes(prop)) { + return + } + + delete newConf[prop] + }) + + return newConf + }) + + const conflicts: ConflictedContainerConfigData = {} + + configs.forEach(one => { + const others = configs.filter(it => it !== one) + + others.forEach(other => collectConflicts(conflicts, one, other)) + }) + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const UNINTERESTED_KEYS = ['id', 'type', 'updatedAt', 'updatedBy', 'secrets'] +export const getConflictsForConcreteConfig = ( + configs: ContainerConfigDataWithId[], + concreteConfig: ConcreteContainerConfigData, +): ConflictedContainerConfigData | null => + checkForConflicts( + configs, + Object.entries(concreteConfig) + .filter(entry => { + const [key, value] = entry + if (UNINTERESTED_KEYS.includes(key)) { + return false + } + + return typeof value !== 'undefined' && value !== null + }) + .map(entry => { + const [key] = entry + return key + }) as ContainerConfigDataProperty[], + ) diff --git a/web/crux/src/domain/container-merge.spec.ts b/web/crux/src/domain/container-merge.spec.ts new file mode 100644 index 0000000000..a85e7ad440 --- /dev/null +++ b/web/crux/src/domain/container-merge.spec.ts @@ -0,0 +1,530 @@ +import { ConcreteContainerConfigData, ContainerConfigData } from './container' +import { mergeConfigsWithConcreteConfig } from './container-merge' + +describe('container-merge', () => { + const fullConfig: ContainerConfigData = { + name: 'img', + capabilities: [], + deploymentStrategy: 'recreate', + workingDirectory: '/app', + expose: 'expose', + networkMode: 'bridge', + proxyHeaders: false, + restartPolicy: 'no', + tty: false, + useLoadBalancer: false, + annotations: { + deployment: [ + { + id: 'annotations.deployment', + key: 'annotations.deployment', + value: 'annotations.deployment', + }, + ], + ingress: [ + { + id: 'annotations.ingress', + key: 'annotations.ingress', + value: 'annotations.ingress', + }, + ], + service: [ + { + id: 'annotations.service', + key: 'annotations.service', + value: 'annotations.service', + }, + ], + }, + labels: { + deployment: [ + { + id: 'labels.deployment', + key: 'labels.deployment', + value: 'labels.deployment', + }, + ], + ingress: [ + { + id: 'labels.ingress', + key: 'labels.ingress', + value: 'labels.ingress', + }, + ], + service: [ + { + id: 'labels.service', + key: 'labels.service', + value: 'labels.service', + }, + ], + }, + args: [ + { + id: 'arg1', + key: 'arg1', + }, + ], + commands: [ + { + id: 'command1', + key: 'command1', + }, + ], + configContainer: { + image: 'configCont', + keepFiles: false, + path: 'configCont', + volume: 'configCont', + }, + customHeaders: [ + { + id: 'customHead', + key: 'customHead', + }, + ], + dockerLabels: [ + { + id: 'dockerLabel1', + key: 'dockerLabel1', + value: 'dockerLabel1', + }, + ], + environment: [ + { + id: 'env1', + key: 'env1', + value: 'env1', + }, + ], + extraLBAnnotations: [ + { + id: 'lbAnn1', + key: 'lbAnn1', + value: 'lbAnn1', + }, + ], + healthCheckConfig: { + livenessProbe: 'healthCheckConf', + port: 1, + readinessProbe: 'healthCheckConf', + startupProbe: 'healthCheckConf', + }, + storageSet: true, + storageId: 'storageId', + storageConfig: { + bucket: 'storageBucket', + path: 'storagePath', + }, + routing: { + domain: 'domain', + path: 'path', + stripPrefix: true, + uploadLimit: 'uploadLimit', + }, + initContainers: [ + { + id: 'initCont1', + args: [ + { + id: 'initCont1Args', + key: 'initCont1Args', + }, + ], + command: [ + { + id: 'initCont1Command', + key: 'initCont1Command', + }, + ], + environment: [ + { + id: 'initCont1Env', + key: 'initCont1Env', + value: 'initCont1Env', + }, + ], + image: 'initCont1', + name: 'initCont1', + useParentConfig: false, + volumes: [ + { + id: 'initCont1Vol1', + name: 'initCont1Vol1', + path: 'initCont1Vol1', + }, + ], + }, + ], + logConfig: { + driver: 'awslogs', + options: [ + { + id: 'logConfOps', + key: 'logConfOps', + value: 'logConfOps', + }, + ], + }, + networks: [ + { + id: 'network1', + key: 'network1', + }, + ], + portRanges: [ + { + id: 'portRange1', + external: { + from: 1, + to: 2, + }, + internal: { + from: 1, + to: 2, + }, + }, + ], + ports: [ + { + id: 'port1', + internal: 1, + external: 1, + }, + ], + resourceConfig: { + limits: { + cpu: 'limitCpu', + memory: 'limitMemory', + }, + requests: { + cpu: 'requestCpu', + memory: 'requestMemory', + }, + }, + secrets: [ + { + id: 'secret1', + key: 'secret1', + required: false, + }, + ], + user: 1, + volumes: [ + { + id: 'vol1', + name: 'vol1', + path: 'vol1', + class: 'vol1', + size: 'vol1', + type: 'mem', + }, + ], + metrics: undefined, + expectedState: undefined, + } + + const fullConcreteConfig: ConcreteContainerConfigData = { + name: 'instance.img', + capabilities: [], + deploymentStrategy: 'recreate', + workingDirectory: '/app', + expose: 'exposeWithTls', + networkMode: 'host', + proxyHeaders: true, + restartPolicy: 'onFailure', + tty: true, + useLoadBalancer: true, + annotations: { + deployment: [ + { + id: 'instance.annotations.deployment', + key: 'instance.annotations.deployment', + value: 'instance.annotations.deployment', + }, + ], + ingress: [ + { + id: 'instance.annotations.ingress', + key: 'instance.annotations.ingress', + value: 'instance.annotations.ingress', + }, + ], + service: [ + { + id: 'instance.annotations.service', + key: 'instance.annotations.service', + value: 'instance.annotations.service', + }, + ], + }, + labels: { + deployment: [ + { + id: 'instance.labels.deployment', + key: 'instance.labels.deployment', + value: 'instance.labels.deployment', + }, + ], + ingress: [ + { + id: 'instance.labels.ingress', + key: 'instance.labels.ingress', + value: 'instance.labels.ingress', + }, + ], + service: [ + { + id: 'instance.labels.service', + key: 'instance.labels.service', + value: 'instance.labels.service', + }, + ], + }, + args: [ + { + id: 'instance.arg1', + key: 'instance.arg1', + }, + ], + commands: [ + { + id: 'instance.command1', + key: 'instance.command1', + }, + ], + configContainer: { + image: 'instance.configCont', + keepFiles: true, + path: 'instance.configCont', + volume: 'instance.configCont', + }, + customHeaders: [ + { + id: 'instance.customHead', + key: 'instance.customHead', + }, + ], + dockerLabels: [ + { + id: 'instance.dockerLabel1', + key: 'instance.dockerLabel1', + value: 'instance.dockerLabel1', + }, + ], + environment: [ + { + id: 'instance.env1', + key: 'instance.env1', + value: 'instance.env1', + }, + ], + extraLBAnnotations: [ + { + id: 'instance.lbAnn1', + key: 'instance.lbAnn1', + value: 'instance.lbAnn1', + }, + ], + healthCheckConfig: { + livenessProbe: 'instance.healthCheckConf', + port: 1, + readinessProbe: 'instance.healthCheckConf', + startupProbe: 'instance.healthCheckConf', + }, + storageSet: true, + storageId: 'instance.storageId', + storageConfig: { + bucket: 'instance.storageBucket', + path: 'instance.storagePath', + }, + routing: { + domain: 'instance.domain', + path: 'instance.path', + stripPrefix: true, + uploadLimit: 'instance.uploadLimit', + }, + initContainers: [ + { + id: 'instance.initCont1', + args: [ + { + id: 'instance.initCont1Args', + key: 'instance.initCont1Args', + }, + ], + command: [ + { + id: 'instance.initCont1Command', + key: 'instance.initCont1Command', + }, + ], + environment: [ + { + id: 'instance.initCont1Env', + key: 'instance.initCont1Env', + value: 'instance.initCont1Env', + }, + ], + image: 'instance.initCont1', + name: 'instance.initCont1', + useParentConfig: true, + volumes: [ + { + id: 'instance.initCont1Vol1', + name: 'instance.initCont1Vol1', + path: 'instance.initCont1Vol1', + }, + ], + }, + ], + logConfig: { + driver: 'gcplogs', + options: [ + { + id: 'instance.logConfOps', + key: 'instance.logConfOps', + value: 'instance.logConfOps', + }, + ], + }, + networks: [ + { + id: 'instance.network1', + key: 'instance.network1', + }, + ], + portRanges: [ + { + id: 'instance.portRange1', + external: { + from: 10, + to: 20, + }, + internal: { + from: 10, + to: 20, + }, + }, + ], + ports: [ + { + id: 'instance.port1', + internal: 10, + external: 10, + }, + ], + resourceConfig: { + limits: { + cpu: 'instance.limitCpu', + memory: 'instance.limitMemory', + }, + requests: { + cpu: 'instance.requestCpu', + memory: 'instance.requestMemory', + }, + }, + secrets: [ + { + id: 'secret1', + key: 'instance.secret1', + required: false, + encrypted: true, + value: 'instance.secret1.publicKey', + publicKey: 'instance.secret1.publicKey', + }, + ], + user: 1, + volumes: [ + { + id: 'instance.vol1', + name: 'instance.vol1', + path: 'instance.vol1', + class: 'instance.vol1', + size: 'instance.vol1', + type: 'rwo', + }, + ], + metrics: undefined, + expectedState: undefined, + } + + describe('mergeConfigsWithConcreteConfig', () => { + it('should use the concrete variables when available', () => { + const merged = mergeConfigsWithConcreteConfig([fullConfig], fullConcreteConfig) + + expect(merged).toEqual(fullConcreteConfig) + }) + + it('should use the config variables when the concrete one is not available', () => { + const merged = mergeConfigsWithConcreteConfig([fullConfig], {}) + + const expected: ConcreteContainerConfigData = { + ...fullConfig, + secrets: [ + { + id: 'secret1', + key: 'secret1', + required: false, + encrypted: false, + value: '', + publicKey: null, + }, + ], + } + + expect(merged).toEqual(expected) + }) + + it('should use the instance only when available', () => { + const instance: ConcreteContainerConfigData = { + ports: fullConcreteConfig.ports, + labels: { + deployment: [ + { + id: 'instance.labels.deployment', + key: 'instance.labels.deployment', + value: 'instance.labels.deployment', + }, + ], + }, + annotations: { + service: [ + { + id: 'instance.annotations.service', + key: 'instance.annotations.service', + value: 'instance.annotations.service', + }, + ], + }, + } + + const expected: ConcreteContainerConfigData = { + ...fullConfig, + ports: fullConcreteConfig.ports, + labels: { + ...fullConfig.labels, + deployment: instance.labels.deployment, + }, + annotations: { + ...fullConfig.annotations, + service: instance.annotations.service, + }, + secrets: [ + { + id: 'secret1', + key: 'secret1', + required: false, + encrypted: false, + value: '', + publicKey: null, + }, + ], + } + + const merged = mergeConfigsWithConcreteConfig([fullConfig], instance) + + expect(merged).toEqual(expected) + }) + }) +}) diff --git a/web/crux/src/domain/container-merge.ts b/web/crux/src/domain/container-merge.ts new file mode 100644 index 0000000000..378952445c --- /dev/null +++ b/web/crux/src/domain/container-merge.ts @@ -0,0 +1,270 @@ +import { + ConcreteContainerConfigData, + ContainerConfigData, + ContainerPortRange, + Marker, + Port, + UniqueKey, + UniqueSecretKey, + UniqueSecretKeyValue, + Volume, +} from './container' +import { rangesOverlap } from './container-conflict' + +const mergeNumber = (strong: number, weak: number): number => { + if (typeof strong === 'number') { + return strong + } + + if (typeof weak === 'number') { + return weak + } + + return null +} + +const mergeBoolean = (strong: boolean, weak: boolean): boolean => { + if (typeof strong === 'boolean') { + return strong + } + + if (typeof weak === 'boolean') { + return weak + } + + return null +} + +type StorageProperties = Pick +const mergeStorage = (strong: StorageProperties, weak: StorageProperties): StorageProperties => { + const set = mergeBoolean(strong.storageSet, weak.storageSet) + if (!set) { + // neither of them are set + + return { + storageSet: false, + storageId: null, + storageConfig: null, + } + } + + if (typeof strong.storageSet === 'boolean') { + // strong is set + + return { + storageSet: true, + storageId: strong.storageId, + storageConfig: strong.storageConfig, + } + } + + return { + storageSet: true, + storageId: weak.storageId, + storageConfig: weak.storageConfig, + } +} + +export const mergeMarkers = (strong: Marker, weak: Marker): Marker => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + return { + deployment: strong.deployment ?? weak.deployment ?? [], + ingress: strong.ingress ?? weak.ingress ?? [], + service: strong.service ?? weak.service ?? [], + } +} + +const mergeSecretKeys = (one: UniqueSecretKey[], other: UniqueSecretKey[]): UniqueSecretKey[] => { + if (!one) { + return other + } + + if (!other) { + return one + } + + return [...one, ...other.filter(it => !one.includes(it))] +} + +export const mergeSecrets = (strong: UniqueSecretKeyValue[], weak: UniqueSecretKey[]): UniqueSecretKeyValue[] => { + weak = weak ?? [] + strong = strong ?? [] + + const overriddenIds: Set = new Set(strong?.map(it => it.id)) + + const missing: UniqueSecretKeyValue[] = weak + .filter(it => !overriddenIds.has(it.id)) + .map(it => ({ + ...it, + value: '', + encrypted: false, + publicKey: null, + })) + + return [...missing, ...strong] +} + +export const mergeConfigs = (strong: ContainerConfigData, weak: ContainerConfigData): ContainerConfigData => ({ + // common + name: strong.name ?? weak.name, + environment: strong.environment ?? weak.environment, + secrets: mergeSecretKeys(strong.secrets, weak.secrets), + user: mergeNumber(strong.user, weak.user), + workingDirectory: strong.workingDirectory ?? weak.workingDirectory, + tty: mergeBoolean(strong.tty, weak.tty), + portRanges: strong.portRanges ?? weak.portRanges, + args: strong.args ?? weak.args, + commands: strong.commands ?? weak.commands, + expose: strong.expose ?? weak.expose, + configContainer: strong.configContainer ?? weak.configContainer, + routing: strong.routing ?? weak.routing, + volumes: strong.volumes ?? weak.volumes, + initContainers: strong.initContainers ?? weak.initContainers, + capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing + ports: strong.ports ?? weak.ports, + ...mergeStorage(strong, weak), + + // crane + customHeaders: strong.customHeaders ?? weak.customHeaders, + proxyHeaders: mergeBoolean(strong.proxyHeaders, weak.proxyHeaders), + extraLBAnnotations: strong.extraLBAnnotations ?? weak.extraLBAnnotations, + healthCheckConfig: strong.healthCheckConfig ?? weak.healthCheckConfig, + resourceConfig: strong.resourceConfig ?? weak.resourceConfig, + useLoadBalancer: mergeBoolean(strong.useLoadBalancer, weak.useLoadBalancer), + deploymentStrategy: strong.deploymentStrategy ?? weak.deploymentStrategy, + labels: mergeMarkers(strong.labels, weak.labels), + annotations: mergeMarkers(strong.annotations, weak.annotations), + metrics: strong.metrics ?? weak.metrics, + + // dagent + logConfig: strong.logConfig ?? weak.logConfig, + networkMode: strong.networkMode ?? weak.networkMode, + restartPolicy: strong.restartPolicy ?? weak.restartPolicy, + networks: strong.networks ?? weak.networks, + dockerLabels: strong.dockerLabels ?? weak.dockerLabels, + expectedState: strong.expectedState ?? weak.expectedState, +}) + +const squashConfigs = (configs: ContainerConfigData[]): ContainerConfigData => + configs.reduce((result, conf) => mergeConfigs(conf, result), {} as ContainerConfigData) + +// this assumes that the concrete config takes care of any conflict between the other configs +export const mergeConfigsWithConcreteConfig = ( + configs: ContainerConfigData[], + concrete: ConcreteContainerConfigData, +): ConcreteContainerConfigData => { + const squashed = squashConfigs(configs.filter(it => !!it)) + concrete = concrete ?? {} + + const baseConfig = mergeConfigs(concrete, squashed) + + return { + ...baseConfig, + secrets: mergeSecrets(concrete.secrets, squashed.secrets), + } +} + +const mergeUniqueKeys = (strong: T[], weak: T[]): T[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.key === w.key)) + return [...strong, ...missing] +} + +const mergePorts = (strong: Port[], weak: Port[]): Port[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.internal === w.internal)) + return [...strong, ...missing] +} + +const mergePortRanges = (strong: ContainerPortRange[], weak: ContainerPortRange[]): ContainerPortRange[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter( + w => !strong.find(it => rangesOverlap(w.internal, it.internal) || rangesOverlap(w.external, it.external)), + ) + return [...strong, ...missing] +} + +const mergeVolumes = (strong: Volume[], weak: Volume[]): Volume[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.path === w.path || it.name === w.path)) + return [...strong, ...missing] +} + +export const mergeInstanceConfigWithDeploymentConfig = ( + deployment: ConcreteContainerConfigData, + instance: ConcreteContainerConfigData, +): ConcreteContainerConfigData => ({ + // common + name: instance.name ?? deployment.name ?? null, + environment: mergeUniqueKeys(instance.environment, deployment.environment), + secrets: mergeUniqueKeys(instance.secrets, deployment.secrets), + user: mergeNumber(instance.user, deployment.user), + workingDirectory: instance.workingDirectory ?? deployment.workingDirectory ?? null, + tty: mergeBoolean(instance.tty, deployment.tty), + ports: mergePorts(instance.ports, deployment.ports), + portRanges: mergePortRanges(instance.portRanges, deployment.portRanges), + args: mergeUniqueKeys(instance.args, deployment.args), + commands: mergeUniqueKeys(instance.commands, deployment.commands), + expose: instance.expose ?? deployment.expose ?? null, + configContainer: instance.configContainer ?? deployment.configContainer ?? null, + routing: instance.routing ?? deployment.routing ?? null, + volumes: mergeVolumes(instance.volumes, deployment.volumes), + initContainers: instance.initContainers ?? deployment.initContainers ?? null, // TODO (@m8vago): merge them correctly after the init container rework + capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing + ...mergeStorage(instance, deployment), + + // crane + customHeaders: mergeUniqueKeys(deployment.customHeaders, instance.customHeaders), + proxyHeaders: mergeBoolean(instance.proxyHeaders, deployment.proxyHeaders), + extraLBAnnotations: mergeUniqueKeys(instance.extraLBAnnotations, deployment.extraLBAnnotations), + healthCheckConfig: instance.healthCheckConfig ?? deployment.healthCheckConfig ?? null, + resourceConfig: instance.resourceConfig ?? deployment.resourceConfig ?? null, + useLoadBalancer: mergeBoolean(instance.useLoadBalancer, deployment.useLoadBalancer), + deploymentStrategy: instance.deploymentStrategy ?? deployment.deploymentStrategy ?? null, + labels: mergeMarkers(instance.labels, deployment.labels), + annotations: mergeMarkers(instance.annotations, deployment.annotations), + metrics: instance.metrics ?? deployment.metrics ?? null, + + // dagent + logConfig: instance.logConfig ?? deployment.logConfig ?? null, + networkMode: instance.networkMode ?? deployment.networkMode ?? null, + restartPolicy: instance.restartPolicy ?? deployment.restartPolicy ?? null, + networks: mergeUniqueKeys(instance.networks, deployment.networks), + dockerLabels: mergeUniqueKeys(instance.dockerLabels, deployment.dockerLabels), + expectedState: instance.expectedState ?? deployment.expectedState ?? null, +}) diff --git a/web/crux/src/domain/container.ts b/web/crux/src/domain/container.ts index 9e4b01da0c..bf371f100c 100644 --- a/web/crux/src/domain/container.ts +++ b/web/crux/src/domain/container.ts @@ -1,4 +1,5 @@ import { NetworkMode } from '@prisma/client' +import { registryImageNameToContainerName } from './image' export const PORT_MIN = 0 export const PORT_MAX = 65535 @@ -164,14 +165,14 @@ export type ExpectedContainerState = { export type ContainerConfigData = { // common - name: string + name?: string environment?: UniqueKeyValue[] secrets?: UniqueSecretKey[] routing?: Routing - expose: ContainerExposeStrategy + expose?: ContainerExposeStrategy user?: number workingDirectory?: string - tty: boolean + tty?: boolean configContainer?: Container ports?: Port[] portRanges?: ContainerPortRange[] @@ -179,24 +180,24 @@ export type ContainerConfigData = { commands?: UniqueKey[] args?: UniqueKey[] initContainers?: InitContainer[] - capabilities: UniqueKeyValue[] + capabilities?: UniqueKeyValue[] storageSet?: boolean storageId?: string storageConfig?: Storage // dagent logConfig?: Log - restartPolicy: ContainerRestartPolicyType - networkMode: NetworkMode + restartPolicy?: ContainerRestartPolicyType + networkMode?: NetworkMode networks?: UniqueKey[] dockerLabels?: UniqueKeyValue[] expectedState?: ExpectedContainerState // crane - deploymentStrategy: ContainerDeploymentStrategyType + deploymentStrategy?: ContainerDeploymentStrategyType customHeaders?: UniqueKey[] - proxyHeaders: boolean - useLoadBalancer: boolean + proxyHeaders?: boolean + useLoadBalancer?: boolean extraLBAnnotations?: UniqueKeyValue[] healthCheckConfig?: HealthCheck resourceConfig?: ResourceConfig @@ -205,8 +206,12 @@ export type ContainerConfigData = { metrics?: Metrics } -type DagentSpecificConfig = 'logConfig' | 'restartPolicy' | 'networkMode' | 'networks' | 'dockerLabels' -type CraneSpecificConfig = +export type ContainerConfigDataWithId = ContainerConfigData & { + id: string +} + +type DagentSpecificConfigKeys = 'logConfig' | 'restartPolicy' | 'networkMode' | 'networks' | 'dockerLabels' +type CraneSpecificConfigKeys = | 'deploymentStrategy' | 'customHeaders' | 'proxyHeaders' @@ -218,16 +223,14 @@ type CraneSpecificConfig = | 'annotations' | 'metrics' -export type DagentConfigDetails = Pick -export type CraneConfigDetails = Pick -export type CommonConfigDetails = Omit +export type DagentConfigDetails = Pick +export type CraneConfigDetails = Pick +export type CommonConfigDetails = Omit -export type MergedContainerConfigData = Omit & { - secrets: UniqueSecretKeyValue[] +export type ConcreteContainerConfigData = Omit & { + secrets?: UniqueSecretKeyValue[] } -export type InstanceContainerConfigData = Partial - export const CONTAINER_CONFIG_JSON_FIELDS = [ // Common 'environment', @@ -264,3 +267,16 @@ export const CONTAINER_CONFIG_DEFAULT_VALUES = { export const CONTAINER_CONFIG_COMPOSITE_FIELDS = { storage: ['storageSet', 'storageId', 'storageConfig'], } + +export const configIsEmpty = (config: T): boolean => Object.keys(config).length < 1 + +type InstanceWithConfigAndImageConfig = { + config: { name: string } + image: { + name: string + config: { name: string } + } +} + +export const nameOfInstance = (instance: InstanceWithConfigAndImageConfig) => + instance.config.name ?? instance.image.config.name ?? registryImageNameToContainerName(instance.image.name) diff --git a/web/crux/src/domain/deployment.spec.ts b/web/crux/src/domain/deployment.spec.ts index 67d38cff6e..1a8a8898fb 100644 --- a/web/crux/src/domain/deployment.spec.ts +++ b/web/crux/src/domain/deployment.spec.ts @@ -2,11 +2,11 @@ import { DeploymentStatusEnum, VersionTypeEnum } from '@prisma/client' import { ContainerState, DeploymentStatus as ProtoDeploymentStatus } from 'src/grpc/protobuf/proto/common' import { checkDeploymentCopiability, - checkDeploymentDeletability, checkDeploymentDeployability, - checkDeploymentMutability, containerNameFromImageName, containerStateToDto, + deploymentIsDeletable, + deploymentIsMutable, deploymentStatusToDb, } from './deployment' @@ -63,7 +63,7 @@ describe('DomainDeployment', () => { ) it.each(DEPLOYMENT_STATUSES)('%p and %p', (status: DeploymentStatusEnum) => { - expect(checkDeploymentDeletability(status)).toEqual(status !== 'inProgress') + expect(deploymentIsDeletable(status)).toEqual(status !== 'inProgress') }) }) @@ -71,7 +71,7 @@ describe('DomainDeployment', () => { it.each(DEPLOYMENT_STATUSES_VERSION_TYPES)( 'should return true if status is deploying or if the status is successful or failed and the version is rolling (%p and %p)', (status: DeploymentStatusEnum, type: VersionTypeEnum) => { - expect(checkDeploymentMutability(status, type)).toEqual( + expect(deploymentIsMutable(status, type)).toEqual( status === 'preparing' || status === 'failed' || (status === 'successful' && type === 'rolling'), ) }, diff --git a/web/crux/src/domain/deployment.ts b/web/crux/src/domain/deployment.ts index ec6804ce08..faaab47333 100644 --- a/web/crux/src/domain/deployment.ts +++ b/web/crux/src/domain/deployment.ts @@ -1,13 +1,20 @@ import { + ConfigBundle, + ContainerConfig, Deployment as DbDeployment, DeploymentEventTypeEnum, DeploymentStatusEnum, + DeploymentToken, + Instance, + Node, + Project, + Version, VersionTypeEnum, } from '.prisma/client' import { Logger } from '@nestjs/common' import { Identity } from '@ory/kratos-client' import { Observable, Subject } from 'rxjs' -import { AgentCommand, VersionDeployRequest } from 'src/grpc/protobuf/proto/agent' +import { AgentCommand, DeployRequest } from 'src/grpc/protobuf/proto/agent' import { DeploymentMessageLevel, DeploymentStatusMessage, @@ -16,7 +23,44 @@ import { containerStateToJSON, deploymentStatusToJSON, } from 'src/grpc/protobuf/proto/common' -import { ContainerState, MergedContainerConfigData, UniqueKeyValue } from './container' +import { BasicProperties } from 'src/shared/dtos/shared.dto' +import { ConcreteContainerConfigData, ContainerState } from './container' +import { ImageDetails } from './image' + +export type DeploymentWithNode = DbDeployment & { + node: Pick +} + +export type DeploymentWithNodeVersion = DeploymentWithNode & { + version: Pick & { + project: Pick + } +} + +export type InstanceDetails = Instance & { + image: ImageDetails + config: ContainerConfig +} + +type ConfigBundleDetails = ConfigBundle & { + config: ContainerConfig +} + +export type DeploymentWithConfig = DbDeployment & { + config: ContainerConfig +} + +export type DeploymentWithConfigAndBundles = DeploymentWithNodeVersion & { + config: ContainerConfig + configBundles: { + configBundle: ConfigBundleDetails + }[] +} + +export type DeploymentDetails = DeploymentWithConfigAndBundles & { + token: Pick + instances: InstanceDetails[] +} export type DeploymentLogLevel = 'info' | 'warn' | 'error' @@ -99,9 +143,9 @@ export const checkPrefixAvailability = ( return !relevantDeployments.find(it => it.status === 'preparing' || it.status === 'inProgress') } -export const checkDeploymentDeletability = (status: DeploymentStatusEnum): boolean => status !== 'inProgress' +export const deploymentIsDeletable = (status: DeploymentStatusEnum): boolean => status !== 'inProgress' -export const checkDeploymentMutability = (status: DeploymentStatusEnum, type: VersionTypeEnum): boolean => { +export const deploymentIsMutable = (status: DeploymentStatusEnum, type: VersionTypeEnum): boolean => { switch (status) { case 'preparing': case 'failed': @@ -134,23 +178,41 @@ export type DeploymentNotification = { nodeName: string } +export type DeploymentOptions = { + request: DeployRequest + notification: DeploymentNotification + instanceConfigs: Map + deploymentConfig: ConcreteContainerConfigData + tries: number +} + export default class Deployment { private statusChannel = new Subject() private status: DeploymentStatusEnum = 'preparing' - readonly id: string + get id(): string { + return this.options.request.id + } + + get notification(): DeploymentNotification { + return this.options.notification + } + + get instanceConfigs(): Map { + return this.options.instanceConfigs + } - constructor( - private readonly request: VersionDeployRequest, - public notification: DeploymentNotification, - public mergedConfigs: Map, - public sharedEnvironment: UniqueKeyValue[], - public readonly tries: number, - ) { - this.id = request.id + get deploymentConfig(): ConcreteContainerConfigData { + return this.options.deploymentConfig } + get tries(): number { + return this.options.tries + } + + constructor(private readonly options: DeploymentOptions) {} + getStatus() { return this.status } @@ -164,7 +226,7 @@ export default class Deployment { }) commandChannel.next({ - deploy: this.request, + deploy: this.options.request, } as AgentCommand) return this.statusChannel.asObservable() diff --git a/web/crux/src/domain/domain-events.ts b/web/crux/src/domain/domain-events.ts new file mode 100644 index 0000000000..59f15e7c75 --- /dev/null +++ b/web/crux/src/domain/domain-events.ts @@ -0,0 +1,58 @@ +import { ConfigBundle } from '@prisma/client' +import { ContainerConfigData } from './container' +import { ImageDetails } from './image' + +// container +export const CONTAINER_CONFIG_EVENT_UPDATE = 'container-config.update' +export const CONTAINER_CONFIG_ANY = 'container-config.*' +export type ContainerConfigUpdatedEvent = { + id: string + patch: ContainerConfigData +} + +// image +export const IMAGE_EVENT_ADD = 'image.add' +export const IMAGE_EVENT_DELETE = 'image.delete' +export const IMAGE_EVENT_ANY = 'image.*' + +export type ImageEvent = { + versionId: string +} + +export type ImagesAddedEvent = ImageEvent & { + images: ImageDetails[] +} + +export type ImageDeletedEvent = Omit & { + imageId: string + instances: { + id: string + configId: string + deploymentId: string + }[] +} + +// deployment +export type DeploymentEditEvent = { + deploymentId: string +} + +export const DEPLOYMENT_EVENT_INSTACE_CREATE = 'deployment.instance.create' +export const DEPLOYMENT_EVENT_INSTACE_DELETE = 'deployment.instance.delete' + +export type InstanceDetails = { + id: string + configId: string + image: ImageDetails +} + +export type InstancesCreatedEvent = DeploymentEditEvent & { + instances: InstanceDetails[] +} + +export type InstanceDeletedEvent = DeploymentEditEvent & Omit + +export const DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE = 'deployment.config-bundles.update' +export type DeploymentConfigBundlesUpdatedEvent = DeploymentEditEvent & { + bundles: ConfigBundle[] +} diff --git a/web/crux/src/domain/image.ts b/web/crux/src/domain/image.ts index 063beb2db6..7ac642a4ae 100644 --- a/web/crux/src/domain/image.ts +++ b/web/crux/src/domain/image.ts @@ -1,3 +1,4 @@ +import { ContainerConfig, Image, Registry } from '@prisma/client' import { CruxInternalServerErrorException } from 'src/exception/crux-exception' export const ENVIRONMENT_VALUE_TYPES = ['string', 'boolean', 'int'] as const @@ -9,6 +10,14 @@ export type EnvironmentRule = { default?: string } +export type ImageWithRegistry = Image & { + registry: Registry +} + +export type ImageDetails = ImageWithRegistry & { + config: ContainerConfig +} + /** * Parse dyrector.io specific image labels which contain environment validation rules. * @@ -67,3 +76,11 @@ export const parseDyrectorioEnvRules = (labels: Record): Record< } }, {}) } + +export const registryImageNameToContainerName = (name: string) => { + if (name.includes('/')) { + return name.split('/').pop() + } + + return name +} diff --git a/web/crux/src/domain/start-deployment.ts b/web/crux/src/domain/start-deployment.ts new file mode 100644 index 0000000000..4114a672b6 --- /dev/null +++ b/web/crux/src/domain/start-deployment.ts @@ -0,0 +1,177 @@ +import { ContainerConfig, DeploymentStatusEnum, VersionTypeEnum } from '@prisma/client' +import { ConcreteContainerConfigData, ContainerConfigData, UniqueSecretKeyValue, nameOfInstance } from './container' +import { mergeConfigsWithConcreteConfig, mergeInstanceConfigWithDeploymentConfig } from './container-merge' +import { DeploymentWithConfig } from './deployment' + +export type InvalidSecrets = { + configId: string + invalid: string[] + secrets: UniqueSecretKeyValue[] +} + +export type MissingSecrets = { + configId: string + secretKeys: string[] +} + +export const missingSecretsOf = (configId: string, config: ConcreteContainerConfigData): MissingSecrets | null => { + if (!config?.secrets) { + return null + } + + const requiredSecrets = config.secrets.filter(it => it.required || (it.value && it.value.length > 0)) + const missingSecrets = requiredSecrets.filter(it => !it.encrypted) + + if (missingSecrets.length < 1) { + return null + } + + return { + configId, + secretKeys: missingSecrets.map(it => it.key), + } +} + +export const collectInvalidSecrets = ( + configId: string, + config: ConcreteContainerConfigData, + publicKey: string, +): InvalidSecrets => { + if (!config?.secrets) { + return null + } + + const secrets = config.secrets as UniqueSecretKeyValue[] + const invalid = secrets.filter(it => it.publicKey !== publicKey).map(secret => secret.id) + + if (invalid.length < 1) { + return null + } + + return { + configId, + invalid, + secrets: secrets.map(secret => { + if (!invalid.includes(secret.id)) { + return secret + } + + return { + ...secret, + value: '', + encrypted: false, + publicKey, + } + }), + } +} + +type DeployableDeployment = { + version: { + type: VersionTypeEnum + } + status: DeploymentStatusEnum + config: ContainerConfig + configBundles: { + configBundle: { + config: ContainerConfig + } + }[] +} +export const deploymentConfigOf = (deployment: DeployableDeployment): ConcreteContainerConfigData => { + if ( + deployment.version.type !== 'rolling' && + (deployment.status === 'successful' || deployment.status === 'obsolete') + ) { + // this is a redeployment of a successful or an obsolete deployment of an incremental version + // we should not merge and use only the concrete configs + + return deployment.config as any as ConcreteContainerConfigData + } + + const configBundles = deployment.configBundles.map(it => it.configBundle.config as any as ContainerConfigData) + const deploymentConfig = deployment.config as any as ConcreteContainerConfigData + return mergeConfigsWithConcreteConfig(configBundles, deploymentConfig) +} + +type DeployableInstance = { + image: { + config: ContainerConfig + name: string + } + config: ContainerConfig +} +export const instanceConfigOf = ( + deployment: DeployableDeployment, + deploymentConfig: ConcreteContainerConfigData, + instance: DeployableInstance, +): ConcreteContainerConfigData => { + if ( + deployment.version.type !== 'rolling' && + (deployment.status === 'successful' || deployment.status === 'obsolete') + ) { + // this is a redeployment of a successful or an obsolete deployment of an incremental version + // we should not merge and use only the concrete configs + + return instance.config as any as ConcreteContainerConfigData + } + + // first we merge the deployment config with the image config to resolve secrets globally + const imageConfig = instance.image.config as any as ContainerConfigData + const mergedDeploymentConfig = mergeConfigsWithConcreteConfig([imageConfig], deploymentConfig) + + // then we merge and override the rest with the instance config + const instanceConfig = instance.config as any as ConcreteContainerConfigData + const result = mergeInstanceConfigWithDeploymentConfig(mergedDeploymentConfig, instanceConfig) + + // set defaults + if (!result.name) { + result.name = nameOfInstance(instance) + } + + return result +} + +type SecretCandidate = { + deployedAt: Date + value: string +} +export const mergePrefixNeighborSecrets = ( + deployments: DeploymentWithConfig[], + publicKey: string, +): Record => { + const result = new Map() + + deployments + .sort((one, other) => other.createdAt.getTime() - one.createdAt.getTime()) + .forEach(depl => { + if (!depl.config.secrets) { + return + } + + const secrets = depl.config.secrets as UniqueSecretKeyValue[] + secrets.forEach(it => { + if (it.publicKey !== publicKey) { + return + } + + const candidate = result.get(it.key) + if (candidate && candidate.deployedAt.getTime() > depl.deployedAt.getTime()) { + // when there is already a deployment for the key, and it's the more recent one + return + } + + result.set(it.key, { + deployedAt: depl.deployedAt, + value: it.value, + }) + }) + }) + + const entries = [...result.entries()].map(entry => { + const [key, candidate] = entry + return [key, candidate.value] + }) + + return Object.fromEntries(entries) +} diff --git a/web/crux/src/domain/utils.ts b/web/crux/src/domain/utils.ts index 67699c6a94..5224912be7 100644 --- a/web/crux/src/domain/utils.ts +++ b/web/crux/src/domain/utils.ts @@ -91,6 +91,22 @@ export const toPrismaJson = (val: T): T | JsonNull => { return val } +export const toNullableNumber = (val: number): number | null => { + if (typeof val === 'number') { + return val + } + + return null +} + +export const toNullableBoolean = (val: boolean): boolean | null => { + if (typeof val === 'boolean') { + return val + } + + return null +} + export const generateNonce = () => randomBytes(128).toString('hex') export const tapOnce = ( diff --git a/web/crux/src/domain/validation.ts b/web/crux/src/domain/validation.ts index ff0efdf5b1..5ae47a66b2 100644 --- a/web/crux/src/domain/validation.ts +++ b/web/crux/src/domain/validation.ts @@ -2,7 +2,7 @@ import { ContainerConfigPortRangeDto } from 'src/app/container/container.dto' import { EnvironmentRule, ImageValidation } from 'src/app/image/image.dto' import { ContainerPort } from 'src/app/node/node.dto' import { CruxBadRequestException } from 'src/exception/crux-exception' -import { UID_MAX } from 'src/shared/const' +import { UID_MAX, UID_MIN } from 'src/shared/const' import * as yup from 'yup' import { CONTAINER_DEPLOYMENT_STRATEGY_VALUES, @@ -66,127 +66,62 @@ const portNumberBaseRule = yup const portNumberOptionalRule = portNumberBaseRule.nullable() const portNumberRule = portNumberBaseRule.required() -const routingRule = yup - .object() - .shape({ - domain: yup.string().nullable(), - path: yup.string().nullable(), - stripPath: yup.bool().nullable(), - uploadLimit: yup.string().nullable(), - }) - .default({}) - .nullable() - -const exposeRule = yup - .mixed() - .oneOf([...CONTAINER_EXPOSE_STRATEGY_VALUES]) - .default('none') - .required() - -const instanceExposeRule = yup - .mixed() - .oneOf([...CONTAINER_EXPOSE_STRATEGY_VALUES, null]) - .nullable() - -const restartPolicyRule = yup - .mixed() - .oneOf([...CONTAINER_RESTART_POLICY_TYPE_VALUES]) - .default('no') - -const instanceRestartPolicyRule = yup - .mixed() - .oneOf([...CONTAINER_RESTART_POLICY_TYPE_VALUES, null]) - .nullable() - -const networkModeRule = yup - .mixed() - .oneOf([...CONTAINER_NETWORK_MODE_VALUES]) - .default('bridge') - .required() - -const instanceNetworkModeRule = yup - .mixed() - .oneOf([...CONTAINER_NETWORK_MODE_VALUES, null]) - .nullable() +const routingRule = yup.object().shape({ + domain: yup.string().nullable(), + path: yup.string().nullable(), + stripPath: yup.bool().nullable(), + uploadLimit: yup.string().nullable(), +}) +const exposeRule = yup.mixed().oneOf([...CONTAINER_EXPOSE_STRATEGY_VALUES]) +const restartPolicyRule = yup.mixed().oneOf([...CONTAINER_RESTART_POLICY_TYPE_VALUES]) +const networkModeRule = yup.mixed().oneOf([...CONTAINER_NETWORK_MODE_VALUES]) const deploymentStrategyRule = yup .mixed() .oneOf([...CONTAINER_DEPLOYMENT_STRATEGY_VALUES]) - .required() -const instanceDeploymentStrategyRule = yup - .mixed() - .oneOf([...CONTAINER_DEPLOYMENT_STRATEGY_VALUES, null]) - .nullable() - -const logDriverRule = yup - .mixed() - .oneOf([...CONTAINER_LOG_DRIVER_VALUES]) - .default('nodeDefault') - -const volumeTypeRule = yup - .mixed() - .oneOf([...CONTAINER_VOLUME_TYPE_VALUES]) - .default('rwo') - -const configContainerRule = yup - .object() - .shape({ - image: yup.string().required(), - volume: yup.string().required(), - path: yup.string().required(), - keepFiles: yup.boolean().default(false).required(), - }) - .default({}) - .nullable() - .optional() - -const healthCheckConfigRule = yup - .object() - .shape({ - port: portNumberRule.nullable().optional(), - livenessProbe: yup.string().nullable().optional(), - readinessProbe: yup.string().nullable().optional(), - startupProbe: yup.string().nullable().optional(), - }) - .default({}) - .optional() - .nullable() - -const resourceConfigRule = yup - .object() - .shape({ - limits: yup - .object() - .shape({ - cpu: yup.string().nullable(), - memory: yup.string().nullable(), - }) - .nullable() - .optional(), - memory: yup - .object() - .shape({ - cpu: yup.string().nullable(), - memory: yup.string().nullable(), - }) - .nullable() - .optional(), - livenessProbe: yup.string().nullable(), - }) - .default({}) - .nullable() - .optional() - -const storageRule = yup - .object() - .shape({ - bucket: yup.string().required(), - path: yup.string().required(), - }) - .default({}) - .nullable() - .optional() +const logDriverRule = yup.mixed().oneOf([...CONTAINER_LOG_DRIVER_VALUES]) + +const volumeTypeRule = yup.mixed().oneOf([...CONTAINER_VOLUME_TYPE_VALUES]) + +const configContainerRule = yup.object().shape({ + image: yup.string().required(), + volume: yup.string().required(), + path: yup.string().required(), + keepFiles: yup.boolean().default(false).required(), +}) + +const healthCheckConfigRule = yup.object().shape({ + port: portNumberRule.nullable().optional(), + livenessProbe: yup.string().nullable().optional(), + readinessProbe: yup.string().nullable().optional(), + startupProbe: yup.string().nullable().optional(), +}) + +const resourceConfigRule = yup.object().shape({ + limits: yup + .object() + .shape({ + cpu: yup.string().nullable(), + memory: yup.string().nullable(), + }) + .nullable() + .optional(), + memory: yup + .object() + .shape({ + cpu: yup.string().nullable(), + memory: yup.string().nullable(), + }) + .nullable() + .optional(), + livenessProbe: yup.string().nullable(), +}) + +const storageRule = yup.object().shape({ + bucket: yup.string().required(), + path: yup.string().required(), +}) const createOverlapTest = ( schema: yup.NumberSchema, @@ -213,7 +148,6 @@ const portConfigRule = yup.mixed().when('portRanges', ([portRanges]) => { external: portNumberOptionalRule, }), ) - .default([]) .nullable() .optional() } @@ -225,49 +159,40 @@ const portConfigRule = yup.mixed().when('portRanges', ([portRanges]) => { external: createOverlapTest(portNumberOptionalRule, portRanges, 'external'), }), ) - .default([]) .nullable() .optional() }) -const portRangeConfigRule = yup - .array( - yup.object().shape({ - internal: yup - .object() - .shape({ - from: portNumberRule, - to: portNumberRule, - }) - .default({}) - .required(), - external: yup - .object() - .shape({ - from: portNumberRule, - to: portNumberRule, - }) - .default({}) - .required(), - }), - ) - .default([]) - .nullable() - .optional() +const portRangeConfigRule = yup.array( + yup.object().shape({ + internal: yup + .object() + .shape({ + from: portNumberRule, + to: portNumberRule, + }) + .default({}) + .required(), + external: yup + .object() + .shape({ + from: portNumberRule, + to: portNumberRule, + }) + .default({}) + .required(), + }), +) -const volumeConfigRule = yup - .array( - yup.object().shape({ - name: yup.string().required(), - path: yup.string().required(), - size: yup.string().nullable(), - class: yup.string().nullable(), - type: volumeTypeRule, - }), - ) - .default([]) - .nullable() - .optional() +const volumeConfigRule = yup.array( + yup.object().shape({ + name: yup.string().required(), + path: yup.string().required(), + size: yup.string().nullable(), + class: yup.string().nullable(), + type: volumeTypeRule, + }), +) const initContainerVolumeLinkRule = yup.array( yup.object().shape({ @@ -276,42 +201,37 @@ const initContainerVolumeLinkRule = yup.array( }), ) -const initContainerRule = yup +const initContainerRule = yup.array( + yup.object().shape({ + name: yup.string().required().matches(/^\S+$/g), + image: yup.string().required(), + command: shellCommandSchema.default([]).nullable(), + args: shellCommandSchema.default([]).nullable(), + environment: uniqueKeyValuesSchema.default([]).nullable(), + useParentConfig: yup.boolean().default(false).required(), + volumes: initContainerVolumeLinkRule.default([]).nullable(), + }), +) + +const logConfigRule = yup.object().shape({ + driver: logDriverRule, + options: uniqueKeyValuesSchema.default([]).nullable(), +}) + +const markerRule = yup.object().shape({ + deployment: uniqueKeyValuesSchema.default([]).nullable(), + service: uniqueKeyValuesSchema.default([]).nullable(), + ingress: uniqueKeyValuesSchema.default([]).nullable(), +}) + +const uniqueSecretKeySchema = yup .array( yup.object().shape({ - name: yup.string().required().matches(/^\S+$/g), - image: yup.string().required(), - command: shellCommandSchema.default([]).nullable(), - args: shellCommandSchema.default([]).nullable(), - environment: uniqueKeyValuesSchema.default([]).nullable(), - useParentConfig: yup.boolean().default(false).required(), - volumes: initContainerVolumeLinkRule.default([]).nullable(), + key: yup.string().required().ensure().matches(/^\S+$/g), // all characters are non-whitespaces }), ) - .default([]) - .nullable() - .optional() - -const logConfigRule = yup - .object() - .shape({ - driver: logDriverRule, - options: uniqueKeyValuesSchema.default([]).nullable(), - }) - .default({}) - .nullable() - .optional() - -const markerRule = yup - .object() - .shape({ - deployment: uniqueKeyValuesSchema.default([]).nullable(), - service: uniqueKeyValuesSchema.default([]).nullable(), - ingress: uniqueKeyValuesSchema.default([]).nullable(), - }) - .default({}) - .nullable() - .optional() + .ensure() + .test('keysAreUnique', 'Keys must be unique', arr => new Set(arr.map(it => it.key)).size === arr.length) const uniqueSecretKeyValuesSchema = yup .array( @@ -354,99 +274,93 @@ const metricsRule = yup.mixed().when(['ports'], ([ports]) => { }) .nullable() .optional() - .default(null) }) -const expectedContainerStateRule = yup - .object() - .shape({ - state: yup.string().default(null).nullable().oneOf(CONTAINER_STATE_VALUES), - timeout: yup.number().default(null).nullable().min(0), - exitCode: yup.number().default(0).nullable().min(-127).max(128), - }) - .default({}) - .nullable() - .optional() - -export const containerConfigSchema = yup.object().shape({ - name: yup.string().required().matches(/^\S+$/g), - environment: uniqueKeyValuesSchema.default([]).nullable(), - secrets: uniqueSecretKeyValuesSchema.default([]).nullable(), - routing: routingRule, - expose: exposeRule, - user: yup.number().default(null).min(-1).max(UID_MAX).nullable(), - workingDirectory: yup.string().nullable().optional().matches(/^\S+$/g), - tty: yup.boolean().default(false).required(), - configContainer: configContainerRule, - ports: portConfigRule, - portRanges: portRangeConfigRule, - volumes: volumeConfigRule, - commands: shellCommandSchema.default([]).nullable(), - args: shellCommandSchema.default([]).nullable(), - initContainers: initContainerRule, - capabilities: uniqueKeyValuesSchema.default([]).nullable(), - storageId: yup.string().default(null).nullable(), - storageConfig: storageRule, +const expectedContainerStateRule = yup.object().shape({ + state: yup.string().default(null).nullable().oneOf(CONTAINER_STATE_VALUES), + timeout: yup.number().default(null).nullable().min(0), + exitCode: yup.number().default(0).nullable().min(-127).max(128), +}) + +const containerConfigSchema = yup.object().shape({ + name: yup.string().optional().nullable().matches(/^\S+$/g), + environment: uniqueKeyValuesSchema.optional().nullable(), + secrets: uniqueSecretKeySchema.optional().nullable(), + routing: routingRule.optional().nullable(), + expose: exposeRule.optional().nullable(), + user: yup.number().default(null).min(UID_MIN).max(UID_MAX).optional().nullable(), + workingDirectory: yup.string().optional().nullable().matches(/^\S+$/g), + tty: yup.boolean().optional().nullable(), + configContainer: configContainerRule.optional().nullable(), + ports: portConfigRule.optional().nullable(), + portRanges: portRangeConfigRule.optional().nullable(), + volumes: volumeConfigRule.optional().nullable(), + commands: shellCommandSchema.optional().nullable(), + args: shellCommandSchema.optional().nullable(), + initContainers: initContainerRule.optional().nullable(), + capabilities: uniqueKeyValuesSchema.optional().nullable(), + storageId: yup.string().optional().nullable(), + storageConfig: storageRule.optional().nullable(), // dagent: - logConfig: logConfigRule, - restartPolicy: restartPolicyRule, - networkMode: networkModeRule, - networks: uniqueKeysOnlySchema.default([]).nullable(), - dockerLabels: uniqueKeyValuesSchema.default([]).nullable(), - expectedState: expectedContainerStateRule, + logConfig: logConfigRule.optional().nullable(), + restartPolicy: restartPolicyRule.optional().nullable(), + networkMode: networkModeRule.optional().nullable(), + networks: uniqueKeysOnlySchema.optional().nullable(), + dockerLabels: uniqueKeyValuesSchema.optional().nullable(), + expectedState: expectedContainerStateRule.optional().nullable(), // crane - deploymentStrategy: deploymentStrategyRule, - customHeaders: uniqueKeysOnlySchema.default([]).nullable(), - proxyHeaders: yup.boolean().default(false).required(), - useLoadBalancer: yup.boolean().default(false).required(), - extraLBAnnotations: uniqueKeyValuesSchema.default([]).nullable(), - healthCheckConfig: healthCheckConfigRule, - resourceConfig: resourceConfigRule, - annotations: markerRule, - labels: markerRule, - metrics: metricsRule, + deploymentStrategy: deploymentStrategyRule.optional().nullable(), + customHeaders: uniqueKeysOnlySchema.optional().nullable(), + proxyHeaders: yup.boolean().optional().nullable(), + useLoadBalancer: yup.boolean().optional().nullable(), + extraLBAnnotations: uniqueKeyValuesSchema.optional().nullable(), + healthCheckConfig: healthCheckConfigRule.optional().nullable(), + resourceConfig: resourceConfigRule.optional().nullable(), + annotations: markerRule.optional().nullable(), + labels: markerRule.optional().nullable(), + metrics: metricsRule.optional().nullable(), }) -export const instanceContainerConfigSchema = yup.object().shape({ - name: yup.string().nullable(), - environment: uniqueKeyValuesSchema.default([]).nullable(), - secrets: uniqueKeyValuesSchema.default([]).nullable(), - routing: routingRule.nullable(), - expose: instanceExposeRule, - user: yup.number().default(null).min(-1).max(UID_MAX).nullable(), - tty: yup.boolean().default(false).nullable(), - configContainer: configContainerRule.nullable(), - ports: portConfigRule.nullable(), - portRanges: portRangeConfigRule.nullable(), - volumes: volumeConfigRule.nullable(), - commands: shellCommandSchema.default([]).nullable(), - args: shellCommandSchema.default([]).nullable(), - initContainers: initContainerRule.nullable(), - capabilities: uniqueKeyValuesSchema.default([]).nullable(), - storageId: yup.string().default(null).nullable(), - storageConfig: storageRule, +export const concreteContainerConfigSchema = yup.object().shape({ + name: yup.string().optional().nullable(), + environment: uniqueKeyValuesSchema.optional().nullable(), + secrets: uniqueSecretKeyValuesSchema.optional().nullable(), + routing: routingRule.optional().nullable(), + expose: exposeRule.optional().nullable(), + user: yup.number().optional().nullable().min(-1).max(UID_MAX), + tty: yup.boolean().optional().nullable(), + configContainer: configContainerRule.optional().nullable(), + ports: portConfigRule.optional().nullable(), + portRanges: portRangeConfigRule.optional().nullable(), + volumes: volumeConfigRule.optional().nullable(), + commands: shellCommandSchema.optional().nullable(), + args: shellCommandSchema.optional().nullable(), + initContainers: initContainerRule.optional().nullable(), + capabilities: uniqueKeyValuesSchema.optional().nullable(), + storageId: yup.string().optional().nullable(), + storageConfig: storageRule.optional().nullable(), // dagent: - logConfig: logConfigRule.nullable(), - restartPolicy: instanceRestartPolicyRule, - networkMode: instanceNetworkModeRule, - networks: uniqueKeysOnlySchema.default([]).nullable(), - dockerLabels: uniqueKeyValuesSchema.default([]).nullable(), - expectedState: expectedContainerStateRule, + logConfig: logConfigRule.optional().nullable(), + restartPolicy: restartPolicyRule.optional().nullable(), + networkMode: networkModeRule.optional().nullable(), + networks: uniqueKeysOnlySchema.optional().nullable(), + dockerLabels: uniqueKeyValuesSchema.optional().nullable(), + expectedState: expectedContainerStateRule.optional().nullable(), // crane - deploymentStrategy: instanceDeploymentStrategyRule, - customHeaders: uniqueKeysOnlySchema.default([]).nullable(), - proxyHeaders: yup.boolean().default(false).nullable(), - useLoadBalancer: yup.boolean().default(false).nullable(), - extraLBAnnotations: uniqueKeyValuesSchema.default([]).nullable(), - healthCheckConfig: healthCheckConfigRule.nullable(), - resourceConfig: resourceConfigRule.nullable(), - annotations: markerRule.nullable(), - labels: markerRule.nullable(), - metrics: metricsRule, + deploymentStrategy: deploymentStrategyRule.optional().nullable(), + customHeaders: uniqueKeysOnlySchema.optional().nullable(), + proxyHeaders: yup.boolean().optional().nullable(), + useLoadBalancer: yup.boolean().optional().nullable(), + extraLBAnnotations: uniqueKeyValuesSchema.optional().nullable(), + healthCheckConfig: healthCheckConfigRule.optional().nullable(), + resourceConfig: resourceConfigRule.optional().nullable(), + annotations: markerRule.optional().nullable(), + labels: markerRule.optional().nullable(), + metrics: metricsRule.optional().nullable(), }) const validateEnvironmentRule = (rule: EnvironmentRule, index: number, env: UniqueKeyValue) => { @@ -519,12 +433,19 @@ const testEnvironment = (validation: ImageValidation, arr: UniqueKeyValue[]) => export const createStartDeploymentSchema = (instanceValidation: Record) => yup.object({ - environment: uniqueKeyValuesSchema, + config: containerConfigSchema, + configBundles: yup.array( + yup.object({ + configBundle: yup.object({ + config: containerConfigSchema, + }), + }), + ), instances: yup .array( yup.object({ id: yup.string(), - config: instanceContainerConfigSchema.nullable(), + config: concreteContainerConfigSchema, }), ) .test( @@ -630,6 +551,17 @@ export const templateSchema = yup.object({ .required(), }) +export const nullifyUndefinedProperties = (candidate: object) => { + if (candidate) { + Object.entries(candidate).forEach(entry => { + const [key, value] = entry + if (typeof value === 'undefined') { + candidate[key] = null + } + }) + } +} + export const yupValidate = (schema: yup.AnySchema, candidate: any) => { try { schema.validateSync(candidate) diff --git a/web/crux/src/domain/version-increase.ts b/web/crux/src/domain/version-increase.ts index 5abe740abd..68f10b6492 100644 --- a/web/crux/src/domain/version-increase.ts +++ b/web/crux/src/domain/version-increase.ts @@ -1,22 +1,15 @@ -import { - ContainerConfig, - Deployment, - DeploymentStatusEnum, - Image, - Instance, - InstanceContainerConfig, - Version, -} from '@prisma/client' +import { ContainerConfig, Deployment, DeploymentStatusEnum, Image, Instance, Version } from '@prisma/client' export type ImageWithConfig = Image & { config: ContainerConfig } type InstanceWithConfig = Instance & { - config: InstanceContainerConfig | null + config: ContainerConfig | null } type DeploymentWithInstances = Deployment & { + config: ContainerConfig | null instances: InstanceWithConfig[] } @@ -25,17 +18,18 @@ export type IncreasableVersion = Version & { deployments: DeploymentWithInstances[] } -type CopiedImageWithConfig = Omit & { +type CopiedImageWithConfig = Image & { originalId: string - config: Omit + config: Omit } -type CopiedInstanceWithConfig = Omit & { +type CopiedInstanceWithConfig = Omit & { originalImageId: string - config: Omit + config: Omit } -type CopiedDeploymentWithInstances = Omit & { +type CopiedDeploymentWithInstances = Deployment & { + config: Omit instances: CopiedInstanceWithConfig[] } @@ -44,22 +38,24 @@ export type IncreasedVersion = Omit { - const newInstance: CopiedInstanceWithConfig = { - originalImageId: instance.imageId, - updatedAt: undefined, - config: null, +const copyConfig = (config: ContainerConfig | null): Omit | null => { + if (!config) { + return null } - if (instance.config) { - const config = { - ...instance.config, - } + const newConf = { + ...config, + } + + delete newConf.id - delete config.id - delete config.instanceId + return newConf +} - newInstance.config = config +const copyInstance = (instance: InstanceWithConfig): CopiedInstanceWithConfig => { + const newInstance: CopiedInstanceWithConfig = { + originalImageId: instance.imageId, + config: copyConfig(instance.config), } return newInstance @@ -67,15 +63,14 @@ const copyInstance = (instance: InstanceWithConfig): CopiedInstanceWithConfig => export const copyDeployment = (deployment: DeploymentWithInstances): CopiedDeploymentWithInstances => { const newDeployment: CopiedDeploymentWithInstances = { - note: deployment.note, - prefix: deployment.prefix, + ...deployment, // default status for deployments is preparing status: DeploymentStatusEnum.preparing, - environment: deployment.environment ?? [], - nodeId: deployment.nodeId, - protected: deployment.protected, + config: copyConfig(deployment.config), tries: 0, instances: [], + createdAt: undefined, + createdBy: undefined, updatedAt: undefined, updatedBy: undefined, } @@ -90,23 +85,12 @@ export const copyDeployment = (deployment: DeploymentWithInstances): CopiedDeplo } const copyImage = (image: ImageWithConfig): CopiedImageWithConfig => { - const config = { - ...image.config, - } - - delete config.id - delete config.imageId + const config = copyConfig(image.config) const newImage: CopiedImageWithConfig = { + ...image, originalId: image.id, - name: image.name, - tag: image.tag, - order: image.order, - registryId: image.registryId, - labels: image.labels, config, - updatedAt: undefined, - updatedBy: undefined, } return newImage diff --git a/web/crux/src/domain/version.ts b/web/crux/src/domain/version.ts index d02d6a4430..cb23c45fb7 100644 --- a/web/crux/src/domain/version.ts +++ b/web/crux/src/domain/version.ts @@ -1,6 +1,7 @@ import { Deployment, DeploymentStatusEnum, ProjectTypeEnum, Version, VersionTypeEnum } from '@prisma/client' import { CruxPreconditionFailedException } from 'src/exception/crux-exception' -import { checkDeploymentMutability } from './deployment' +import { DeploymentWithNode, deploymentIsMutable } from './deployment' +import { ImageDetails } from './image' export type VersionWithName = Pick @@ -8,6 +9,18 @@ export type VersionWithDeployments = Version & { deployments: Deployment[] } +export type VersionWithChildren = Version & { + children: { versionId: string }[] +} + +export type VersionDetails = VersionWithChildren & { + project: { + type: ProjectTypeEnum + } + images: ImageDetails[] + deployments: DeploymentWithNode[] +} + export type VersionIncreasabilityCheckDao = { type: VersionTypeEnum children: { versionId: string }[] @@ -25,7 +38,7 @@ export type VersionDeletabilityCheckDao = VersionMutabilityCheckDao & { } export const versionHasImmutableDeployments = (version: VersionMutabilityCheckDao): boolean => - version.deployments.filter(it => !checkDeploymentMutability(it.status, version.type)).length > 0 + version.deployments.filter(it => !deploymentIsMutable(it.status, version.type)).length > 0 // - 'rolling' versions are not increasable // - an 'incremental' version is increasable, when it does not have any child yet @@ -71,6 +84,4 @@ export const checkVersionMutability = (version: VersionMutabilityCheckDao) => { value: version.id, }) } - - return false } diff --git a/web/crux/src/grpc/protobuf/proto/agent.ts b/web/crux/src/grpc/protobuf/proto/agent.ts index d41320e393..30e29af90c 100644 --- a/web/crux/src/grpc/protobuf/proto/agent.ts +++ b/web/crux/src/grpc/protobuf/proto/agent.ts @@ -9,6 +9,7 @@ import { ContainerInspectResponse, ContainerLogListResponse, ContainerLogMessage, + ContainerOrPrefix, ContainerState, ContainerStateListMessage, DeleteContainersRequest, @@ -108,7 +109,7 @@ export interface AgentInfo { } export interface AgentCommand { - deploy?: VersionDeployRequest | undefined + deploy?: DeployRequest | undefined containerState?: ContainerStateRequest | undefined containerDelete?: ContainerDeleteRequest | undefined deployLegacy?: DeployRequestLegacy | undefined @@ -142,38 +143,25 @@ export interface DeployResponse { started: boolean } -export interface VersionDeployRequest { +export interface DeployRequest { id: string versionName: string releaseNotes: string - requests: DeployRequest[] -} - -/** Request for a keys of existing secrets in a prefix, eg. namespace */ -export interface ListSecretsRequest { - container: ContainerIdentifier | undefined -} - -/** Deploys a single container */ -export interface InstanceConfig { - /** - * prefix mapped into host folder structure, - * used as namespace id - */ prefix: string - /** mount path of instance (docker only) */ - mountPath?: string | undefined - /** environment variable map */ - environment: { [key: string]: string } - /** registry repo prefix */ - repositoryPrefix?: string | undefined + secrets: { [key: string]: string } + requests: DeployWorkloadRequest[] } -export interface InstanceConfig_EnvironmentEntry { +export interface DeployRequest_SecretsEntry { key: string value: string } +/** Request for a keys of existing secrets in a prefix, eg. namespace */ +export interface ListSecretsRequest { + target: ContainerOrPrefix | undefined +} + export interface RegistryAuth { name: string url: string @@ -338,17 +326,12 @@ export interface CommonContainerConfig_SecretsEntry { value: string } -export interface DeployRequest { +export interface DeployWorkloadRequest { id: string - containerName: string - /** InstanceConfig is set for multiple containers */ - instanceConfig: InstanceConfig | undefined /** ContainerConfigs */ common?: CommonContainerConfig | undefined dagent?: DagentContainerConfig | undefined crane?: CraneContainerConfig | undefined - /** Runtime info and requirements of a container */ - runtimeConfig?: string | undefined registry?: string | undefined imageName: string tag: string @@ -435,7 +418,7 @@ function createBaseAgentCommand(): AgentCommand { export const AgentCommand = { fromJSON(object: any): AgentCommand { return { - deploy: isSet(object.deploy) ? VersionDeployRequest.fromJSON(object.deploy) : undefined, + deploy: isSet(object.deploy) ? DeployRequest.fromJSON(object.deploy) : undefined, containerState: isSet(object.containerState) ? ContainerStateRequest.fromJSON(object.containerState) : undefined, containerDelete: isSet(object.containerDelete) ? ContainerDeleteRequest.fromJSON(object.containerDelete) @@ -460,8 +443,7 @@ export const AgentCommand = { toJSON(message: AgentCommand): unknown { const obj: any = {} - message.deploy !== undefined && - (obj.deploy = message.deploy ? VersionDeployRequest.toJSON(message.deploy) : undefined) + message.deploy !== undefined && (obj.deploy = message.deploy ? DeployRequest.toJSON(message.deploy) : undefined) message.containerState !== undefined && (obj.containerState = message.containerState ? ContainerStateRequest.toJSON(message.containerState) : undefined) message.containerDelete !== undefined && @@ -560,27 +542,43 @@ export const DeployResponse = { }, } -function createBaseVersionDeployRequest(): VersionDeployRequest { - return { id: '', versionName: '', releaseNotes: '', requests: [] } +function createBaseDeployRequest(): DeployRequest { + return { id: '', versionName: '', releaseNotes: '', prefix: '', secrets: {}, requests: [] } } -export const VersionDeployRequest = { - fromJSON(object: any): VersionDeployRequest { +export const DeployRequest = { + fromJSON(object: any): DeployRequest { return { id: isSet(object.id) ? String(object.id) : '', versionName: isSet(object.versionName) ? String(object.versionName) : '', releaseNotes: isSet(object.releaseNotes) ? String(object.releaseNotes) : '', - requests: Array.isArray(object?.requests) ? object.requests.map((e: any) => DeployRequest.fromJSON(e)) : [], + prefix: isSet(object.prefix) ? String(object.prefix) : '', + secrets: isObject(object.secrets) + ? Object.entries(object.secrets).reduce<{ [key: string]: string }>((acc, [key, value]) => { + acc[key] = String(value) + return acc + }, {}) + : {}, + requests: Array.isArray(object?.requests) + ? object.requests.map((e: any) => DeployWorkloadRequest.fromJSON(e)) + : [], } }, - toJSON(message: VersionDeployRequest): unknown { + toJSON(message: DeployRequest): unknown { const obj: any = {} message.id !== undefined && (obj.id = message.id) message.versionName !== undefined && (obj.versionName = message.versionName) message.releaseNotes !== undefined && (obj.releaseNotes = message.releaseNotes) + message.prefix !== undefined && (obj.prefix = message.prefix) + obj.secrets = {} + if (message.secrets) { + Object.entries(message.secrets).forEach(([k, v]) => { + obj.secrets[k] = v + }) + } if (message.requests) { - obj.requests = message.requests.map(e => (e ? DeployRequest.toJSON(e) : undefined)) + obj.requests = message.requests.map(e => (e ? DeployWorkloadRequest.toJSON(e) : undefined)) } else { obj.requests = [] } @@ -588,70 +586,35 @@ export const VersionDeployRequest = { }, } -function createBaseListSecretsRequest(): ListSecretsRequest { - return { container: undefined } -} - -export const ListSecretsRequest = { - fromJSON(object: any): ListSecretsRequest { - return { container: isSet(object.container) ? ContainerIdentifier.fromJSON(object.container) : undefined } - }, - - toJSON(message: ListSecretsRequest): unknown { - const obj: any = {} - message.container !== undefined && - (obj.container = message.container ? ContainerIdentifier.toJSON(message.container) : undefined) - return obj - }, -} - -function createBaseInstanceConfig(): InstanceConfig { - return { prefix: '', environment: {} } +function createBaseDeployRequest_SecretsEntry(): DeployRequest_SecretsEntry { + return { key: '', value: '' } } -export const InstanceConfig = { - fromJSON(object: any): InstanceConfig { - return { - prefix: isSet(object.prefix) ? String(object.prefix) : '', - mountPath: isSet(object.mountPath) ? String(object.mountPath) : undefined, - environment: isObject(object.environment) - ? Object.entries(object.environment).reduce<{ [key: string]: string }>((acc, [key, value]) => { - acc[key] = String(value) - return acc - }, {}) - : {}, - repositoryPrefix: isSet(object.repositoryPrefix) ? String(object.repositoryPrefix) : undefined, - } +export const DeployRequest_SecretsEntry = { + fromJSON(object: any): DeployRequest_SecretsEntry { + return { key: isSet(object.key) ? String(object.key) : '', value: isSet(object.value) ? String(object.value) : '' } }, - toJSON(message: InstanceConfig): unknown { + toJSON(message: DeployRequest_SecretsEntry): unknown { const obj: any = {} - message.prefix !== undefined && (obj.prefix = message.prefix) - message.mountPath !== undefined && (obj.mountPath = message.mountPath) - obj.environment = {} - if (message.environment) { - Object.entries(message.environment).forEach(([k, v]) => { - obj.environment[k] = v - }) - } - message.repositoryPrefix !== undefined && (obj.repositoryPrefix = message.repositoryPrefix) + message.key !== undefined && (obj.key = message.key) + message.value !== undefined && (obj.value = message.value) return obj }, } -function createBaseInstanceConfig_EnvironmentEntry(): InstanceConfig_EnvironmentEntry { - return { key: '', value: '' } +function createBaseListSecretsRequest(): ListSecretsRequest { + return { target: undefined } } -export const InstanceConfig_EnvironmentEntry = { - fromJSON(object: any): InstanceConfig_EnvironmentEntry { - return { key: isSet(object.key) ? String(object.key) : '', value: isSet(object.value) ? String(object.value) : '' } +export const ListSecretsRequest = { + fromJSON(object: any): ListSecretsRequest { + return { target: isSet(object.target) ? ContainerOrPrefix.fromJSON(object.target) : undefined } }, - toJSON(message: InstanceConfig_EnvironmentEntry): unknown { + toJSON(message: ListSecretsRequest): unknown { const obj: any = {} - message.key !== undefined && (obj.key = message.key) - message.value !== undefined && (obj.value = message.value) + message.target !== undefined && (obj.target = message.target ? ContainerOrPrefix.toJSON(message.target) : undefined) return obj }, } @@ -1371,20 +1334,17 @@ export const CommonContainerConfig_SecretsEntry = { }, } -function createBaseDeployRequest(): DeployRequest { - return { id: '', containerName: '', instanceConfig: undefined, imageName: '', tag: '' } +function createBaseDeployWorkloadRequest(): DeployWorkloadRequest { + return { id: '', imageName: '', tag: '' } } -export const DeployRequest = { - fromJSON(object: any): DeployRequest { +export const DeployWorkloadRequest = { + fromJSON(object: any): DeployWorkloadRequest { return { id: isSet(object.id) ? String(object.id) : '', - containerName: isSet(object.containerName) ? String(object.containerName) : '', - instanceConfig: isSet(object.instanceConfig) ? InstanceConfig.fromJSON(object.instanceConfig) : undefined, common: isSet(object.common) ? CommonContainerConfig.fromJSON(object.common) : undefined, dagent: isSet(object.dagent) ? DagentContainerConfig.fromJSON(object.dagent) : undefined, crane: isSet(object.crane) ? CraneContainerConfig.fromJSON(object.crane) : undefined, - runtimeConfig: isSet(object.runtimeConfig) ? String(object.runtimeConfig) : undefined, registry: isSet(object.registry) ? String(object.registry) : undefined, imageName: isSet(object.imageName) ? String(object.imageName) : '', tag: isSet(object.tag) ? String(object.tag) : '', @@ -1392,18 +1352,14 @@ export const DeployRequest = { } }, - toJSON(message: DeployRequest): unknown { + toJSON(message: DeployWorkloadRequest): unknown { const obj: any = {} message.id !== undefined && (obj.id = message.id) - message.containerName !== undefined && (obj.containerName = message.containerName) - message.instanceConfig !== undefined && - (obj.instanceConfig = message.instanceConfig ? InstanceConfig.toJSON(message.instanceConfig) : undefined) message.common !== undefined && (obj.common = message.common ? CommonContainerConfig.toJSON(message.common) : undefined) message.dagent !== undefined && (obj.dagent = message.dagent ? DagentContainerConfig.toJSON(message.dagent) : undefined) message.crane !== undefined && (obj.crane = message.crane ? CraneContainerConfig.toJSON(message.crane) : undefined) - message.runtimeConfig !== undefined && (obj.runtimeConfig = message.runtimeConfig) message.registry !== undefined && (obj.registry = message.registry) message.imageName !== undefined && (obj.imageName = message.imageName) message.tag !== undefined && (obj.tag = message.tag) diff --git a/web/crux/src/grpc/protobuf/proto/common.ts b/web/crux/src/grpc/protobuf/proto/common.ts index 3638025543..14e47a0410 100644 --- a/web/crux/src/grpc/protobuf/proto/common.ts +++ b/web/crux/src/grpc/protobuf/proto/common.ts @@ -668,11 +668,14 @@ export interface KeyValue { value: string } +export interface ContainerOrPrefix { + container?: ContainerIdentifier | undefined + prefix?: string | undefined +} + export interface ListSecretsResponse { - prefix: string - name: string + target: ContainerOrPrefix | undefined publicKey: string - hasKeys: boolean keys: string[] } @@ -692,8 +695,7 @@ export interface ContainerCommandRequest { } export interface DeleteContainersRequest { - container?: ContainerIdentifier | undefined - prefix?: string | undefined + target: ContainerOrPrefix | undefined } export const COMMON_PACKAGE_NAME = 'common' @@ -1101,27 +1103,44 @@ export const KeyValue = { }, } +function createBaseContainerOrPrefix(): ContainerOrPrefix { + return {} +} + +export const ContainerOrPrefix = { + fromJSON(object: any): ContainerOrPrefix { + return { + container: isSet(object.container) ? ContainerIdentifier.fromJSON(object.container) : undefined, + prefix: isSet(object.prefix) ? String(object.prefix) : undefined, + } + }, + + toJSON(message: ContainerOrPrefix): unknown { + const obj: any = {} + message.container !== undefined && + (obj.container = message.container ? ContainerIdentifier.toJSON(message.container) : undefined) + message.prefix !== undefined && (obj.prefix = message.prefix) + return obj + }, +} + function createBaseListSecretsResponse(): ListSecretsResponse { - return { prefix: '', name: '', publicKey: '', hasKeys: false, keys: [] } + return { target: undefined, publicKey: '', keys: [] } } export const ListSecretsResponse = { fromJSON(object: any): ListSecretsResponse { return { - prefix: isSet(object.prefix) ? String(object.prefix) : '', - name: isSet(object.name) ? String(object.name) : '', + target: isSet(object.target) ? ContainerOrPrefix.fromJSON(object.target) : undefined, publicKey: isSet(object.publicKey) ? String(object.publicKey) : '', - hasKeys: isSet(object.hasKeys) ? Boolean(object.hasKeys) : false, keys: Array.isArray(object?.keys) ? object.keys.map((e: any) => String(e)) : [], } }, toJSON(message: ListSecretsResponse): unknown { const obj: any = {} - message.prefix !== undefined && (obj.prefix = message.prefix) - message.name !== undefined && (obj.name = message.name) + message.target !== undefined && (obj.target = message.target ? ContainerOrPrefix.toJSON(message.target) : undefined) message.publicKey !== undefined && (obj.publicKey = message.publicKey) - message.hasKeys !== undefined && (obj.hasKeys = message.hasKeys) if (message.keys) { obj.keys = message.keys.map(e => e) } else { @@ -1190,22 +1209,17 @@ export const ContainerCommandRequest = { } function createBaseDeleteContainersRequest(): DeleteContainersRequest { - return {} + return { target: undefined } } export const DeleteContainersRequest = { fromJSON(object: any): DeleteContainersRequest { - return { - container: isSet(object.container) ? ContainerIdentifier.fromJSON(object.container) : undefined, - prefix: isSet(object.prefix) ? String(object.prefix) : undefined, - } + return { target: isSet(object.target) ? ContainerOrPrefix.fromJSON(object.target) : undefined } }, toJSON(message: DeleteContainersRequest): unknown { const obj: any = {} - message.container !== undefined && - (obj.container = message.container ? ContainerIdentifier.toJSON(message.container) : undefined) - message.prefix !== undefined && (obj.prefix = message.prefix) + message.target !== undefined && (obj.target = message.target ? ContainerOrPrefix.toJSON(message.target) : undefined) return obj }, } diff --git a/web/crux/src/interceptors/prisma-error-interceptor.ts b/web/crux/src/interceptors/prisma-error-interceptor.ts index ff09645abf..ab87e573c7 100644 --- a/web/crux/src/interceptors/prisma-error-interceptor.ts +++ b/web/crux/src/interceptors/prisma-error-interceptor.ts @@ -94,7 +94,6 @@ export default class PrismaErrorInterceptor implements NestInterceptor { DeploymentEvent: 'deploymentEvent', DeploymentToken: 'deploymentToken', Instance: 'instance', - InstanceContainerConfig: 'instanceConfig', UserInvitation: 'invitation', VersionsOnParentVersion: 'versionRelation', UsersOnTeams: 'team', diff --git a/web/crux/src/shared/const.ts b/web/crux/src/shared/const.ts index 6c3f793729..10f7a1e68b 100644 --- a/web/crux/src/shared/const.ts +++ b/web/crux/src/shared/const.ts @@ -33,6 +33,7 @@ export const API_CREATED_LOCATION_HEADERS = { }, } +export const UID_MIN = -1 export const UID_MAX = 2147483647 export const KRATOS_LIST_PAGE_SIZE = 128 diff --git a/web/crux/src/shared/domain-event.ts b/web/crux/src/shared/domain-event.ts new file mode 100644 index 0000000000..cc36b08a9c --- /dev/null +++ b/web/crux/src/shared/domain-event.ts @@ -0,0 +1,4 @@ +export type DomainEvent = { + type: string + event: T +} diff --git a/web/crux/src/websockets/common.ts b/web/crux/src/websockets/common.ts index 8a2a44f9d1..37f2363e54 100644 --- a/web/crux/src/websockets/common.ts +++ b/web/crux/src/websockets/common.ts @@ -49,6 +49,7 @@ export type WsClientCallbacks = { } export interface WsSubscription { + getCompleter(clientToken: string): Observable getParameter(name: string): string onMessage(client: WsClient, message: WsMessage): Observable sendToAll(message: WsMessage): void diff --git a/web/crux/src/websockets/namespace.ts b/web/crux/src/websockets/namespace.ts index 6a28f2b28b..0c5d45b4dc 100644 --- a/web/crux/src/websockets/namespace.ts +++ b/web/crux/src/websockets/namespace.ts @@ -38,6 +38,11 @@ export default class WsNamespace implements WsSubscription { this.logger.verbose('Closed') } + getCompleter(clientToken: string): Observable { + const resources = this.clients.get(clientToken) + return resources.completer as Observable + } + getParameter(name: string): string | null { return this.params[name] ?? null }