diff --git a/Dockerfile b/Dockerfile index f28afc35..156d4731 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,50 @@ -############################ -# STEP 1 build executable binary -############################ -FROM --platform=$BUILDPLATFORM golang:alpine AS builder +FROM --platform=$BUILDPLATFORM golang:alpine AS build-0 + # Install git. -RUN apk update && apk add --no-cache git=~2 +RUN apk update && \ + apk add --no-cache git=~2 # Set up working directory WORKDIR /app + # Copy go.mod and go.sum separately so we only invalidate the downloading layers if we need to COPY go.mod go.sum ./ # Fetch dependencies and build the binary ENV GO111MODULE=on -RUN go mod download +ENV CGO_ENABLED=0 + +RUN go mod download -x # Copy the rest of the project to ensure code changes doesnt trigger re-download of all deps COPY . . -RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -a -installsuffix cgo -o main . +FROM --platform=$BUILDPLATFORM build-0 AS build-1 +ARG TARGETOS +ARG TARGETARCH -############################ -# STEP 2 build a small image -############################ +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -a -installsuffix cgo -o go-deploy_${TARGETOS}_${TARGETARCH} . + + +# Runner FROM alpine:3 # Set up the working directory WORKDIR /go +ARG TARGETOS +ARG TARGETARCH + # Copy the binary from the builder stage -COPY --from=builder /app/main . +COPY --from=build-1 --chmod=777 /app/go-deploy_${TARGETOS}_${TARGETARCH} /usr/bin/go-deploy # Copy the "index" folder -COPY --from=builder /app/index index +COPY --from=build-1 /app/index index # Copy the "docs" folder -COPY --from=builder /app/docs docs +COPY --from=build-1 /app/docs docs # Set environment variables and expose necessary port ENV PORT=8080 @@ -43,5 +52,5 @@ ENV GIN_MODE=release EXPOSE 8080 # Run the Go Gin binary -ENTRYPOINT ["./main"] +ENTRYPOINT ["/usr/bin/go-deploy"] diff --git a/Makefile b/Makefile index 85f11e0c..41c10e8a 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ all: build build: @echo "Building the application..." @mkdir -p $(BUILD_DIR) - @CGO_ENABLED=0 go build -o $(BUILD_DIR)/$(BINARY_NAME)$(EXT) . + @CGO_ENABLED=1 go build -ldflags="-X github.com/NVIDIA/k8s-dra-driver-gpu/internal/info.version=v1.34.2" -o $(BUILD_DIR)/$(BINARY_NAME)$(EXT) . @echo "Build complete." run: build diff --git a/cmd/server.go b/cmd/server.go index f90af600..cd014caf 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -5,6 +5,7 @@ import ( "errors" argFlag "flag" "fmt" + "net" "net/http" "os" "time" @@ -19,9 +20,11 @@ import ( "github.com/kthcloud/go-deploy/pkg/log" "github.com/kthcloud/go-deploy/pkg/metrics" "github.com/kthcloud/go-deploy/routers" + "github.com/kthcloud/go-deploy/service/utils" ) type Options struct { + Ctx context.Context Flags FlagDefinitionList Mode string } @@ -74,8 +77,7 @@ func Create(opts *Options) *App { } log.Printf("%sInitialization completed%s", log.Orange, log.Reset) - ctx, cancel := context.WithCancel(context.Background()) - + ctx, cancel := context.WithCancel(utils.FirstNonZero(opts.Ctx, context.Background())) for _, flag := range opts.Flags { // Handle api worker separately if flag.Name == "api" { @@ -100,6 +102,9 @@ func Create(opts *Options) *App { httpServer = &http.Server{ Addr: fmt.Sprintf("0.0.0.0:%d", config.Config.Port), Handler: routers.NewRouter(), + BaseContext: func(_ net.Listener) context.Context { + return ctx + }, } go func() { @@ -126,11 +131,14 @@ func (app *App) Stop() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := app.httpServer.Shutdown(ctx); err != nil { - log.Fatalln(fmt.Errorf("failed to shutdown server. details: %w", err)) + log.Errorln(fmt.Errorf("failed to gracefully shutdown server. details: %w", err)) + if closeErr := app.httpServer.Close(); closeErr != nil { + log.Fatalln("Force close failed:", closeErr) + } } <-ctx.Done() - log.Println("Saiting for http server to shutdown...") + log.Println("Waiting for http server to shutdown...") } shutdown() diff --git a/docs/api/v2/V2_docs.go b/docs/api/v2/V2_docs.go index b06eb519..711a59ab 100644 --- a/docs/api/v2/V2_docs.go +++ b/docs/api/v2/V2_docs.go @@ -6,10 +6,7246 @@ import "github.com/swaggo/swag/v2" const docTemplateV2 = `{ "schemes": {{ marshal .Schemes }}, - "components": {"schemas":{"body.ApiKey":{"properties":{"createdAt":{"type":"string"},"expiresAt":{"type":"string"},"name":{"type":"string"}},"type":"object"},"body.ApiKeyCreate":{"properties":{"expiresAt":{"type":"string"},"name":{"type":"string"}},"required":["expiresAt","name"],"type":"object"},"body.ApiKeyCreated":{"properties":{"createdAt":{"type":"string"},"expiresAt":{"type":"string"},"key":{"type":"string"},"name":{"type":"string"}},"type":"object"},"body.BindingError":{"properties":{"validationErrors":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"}},"type":"object"},"body.CiConfig":{"properties":{"config":{"type":"string"}},"type":"object"},"body.ClusterCapacities":{"properties":{"cluster":{"type":"string"},"cpuCore":{"$ref":"#/components/schemas/body.CpuCoreCapacities"},"gpu":{"$ref":"#/components/schemas/body.GpuCapacities"},"ram":{"$ref":"#/components/schemas/body.RamCapacities"}},"type":"object"},"body.ClusterStats":{"properties":{"cluster":{"type":"string"},"podCount":{"type":"integer"}},"type":"object"},"body.CpuCoreCapacities":{"description":"Total","properties":{"total":{"type":"integer"}},"type":"object"},"body.CpuStatus":{"properties":{"load":{"$ref":"#/components/schemas/body.CpuStatusLoad"},"temp":{"$ref":"#/components/schemas/body.CpuStatusTemp"}},"type":"object"},"body.CpuStatusLoad":{"properties":{"cores":{"items":{"type":"integer"},"type":"array","uniqueItems":false},"main":{"type":"number"},"max":{"type":"number"}},"type":"object"},"body.CpuStatusTemp":{"properties":{"cores":{"items":{"type":"integer"},"type":"array","uniqueItems":false},"main":{"type":"number"},"max":{"type":"number"}},"type":"object"},"body.CustomDomainRead":{"properties":{"domain":{"type":"string"},"secret":{"type":"string"},"status":{"type":"string"},"url":{"type":"string"}},"type":"object"},"body.DeploymentCommand":{"properties":{"command":{"enum":["restart"],"type":"string"}},"required":["command"],"type":"object"},"body.DeploymentCreate":{"properties":{"args":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"cpuCores":{"type":"number"},"customDomain":{"description":"CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.","type":"string"},"envs":{"items":{"$ref":"#/components/schemas/body.Env"},"maxItems":1000,"minItems":0,"type":"array","uniqueItems":false},"healthCheckPath":{"maxLength":1000,"minLength":0,"type":"string"},"image":{"maxLength":1000,"minLength":1,"type":"string"},"initCommands":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"name":{"maxLength":30,"minLength":3,"type":"string"},"neverStale":{"description":"Boolean to make deployment never get disabled, despite being stale","type":"boolean"},"private":{"description":"Deprecated: Use Visibility instead.","type":"boolean"},"ram":{"type":"number"},"replicas":{"maximum":100,"minimum":0,"type":"integer"},"visibility":{"enum":["public","private","auth"],"type":"string"},"volumes":{"items":{"$ref":"#/components/schemas/body.Volume"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"zone":{"description":"Zone is the zone that the deployment will be created in.\nIf the zone is not set, the deployment will be created in the default zone.","type":"string"}},"required":["name"],"type":"object"},"body.DeploymentCreated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.DeploymentRead":{"properties":{"accessedAt":{"type":"string"},"args":{"items":{"type":"string"},"type":"array","uniqueItems":false},"createdAt":{"type":"string"},"customDomain":{"$ref":"#/components/schemas/body.CustomDomainRead"},"envs":{"items":{"$ref":"#/components/schemas/body.Env"},"type":"array","uniqueItems":false},"error":{"type":"string"},"healthCheckPath":{"type":"string"},"id":{"type":"string"},"image":{"type":"string"},"initCommands":{"items":{"type":"string"},"type":"array","uniqueItems":false},"integrations":{"description":"Integrations are currently not used, but could be used if we wanted to add a list of integrations to the deployment\n\nFor example GitHub","items":{"type":"string"},"type":"array","uniqueItems":false},"internalPort":{"type":"integer"},"internalPorts":{"items":{"type":"integer"},"type":"array","uniqueItems":false},"name":{"type":"string"},"neverStale":{"type":"boolean"},"ownerId":{"type":"string"},"pingResult":{"type":"integer"},"private":{"description":"Deprecated: Use Visibility instead.","type":"boolean"},"repairedAt":{"type":"string"},"replicaStatus":{"$ref":"#/components/schemas/body.ReplicaStatus"},"restartedAt":{"type":"string"},"specs":{"$ref":"#/components/schemas/body.DeploymentSpecs"},"status":{"type":"string"},"storageUrl":{"type":"string"},"teams":{"items":{"type":"string"},"type":"array","uniqueItems":false},"type":{"type":"string"},"updatedAt":{"type":"string"},"url":{"type":"string"},"visibility":{"type":"string"},"volumes":{"items":{"$ref":"#/components/schemas/body.Volume"},"type":"array","uniqueItems":false},"zone":{"type":"string"}},"type":"object"},"body.DeploymentSpecs":{"properties":{"cpuCores":{"type":"number"},"ram":{"type":"number"},"replicas":{"type":"integer"}},"type":"object"},"body.DeploymentUpdate":{"properties":{"args":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"cpuCores":{"type":"number"},"customDomain":{"description":"CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.","type":"string"},"envs":{"items":{"$ref":"#/components/schemas/body.Env"},"maxItems":1000,"minItems":0,"type":"array","uniqueItems":false},"healthCheckPath":{"maxLength":1000,"minLength":0,"type":"string"},"image":{"maxLength":1000,"minLength":1,"type":"string"},"initCommands":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"name":{"maxLength":30,"minLength":3,"type":"string"},"neverStale":{"type":"boolean"},"private":{"description":"Deprecated: Use Visibility instead.","type":"boolean"},"ram":{"type":"number"},"replicas":{"maximum":100,"minimum":0,"type":"integer"},"visibility":{"enum":["public","private","auth"],"type":"string"},"volumes":{"items":{"$ref":"#/components/schemas/body.Volume"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false}},"required":["name"],"type":"object"},"body.DeploymentUpdated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.DiscoverRead":{"properties":{"roles":{"items":{"$ref":"#/components/schemas/body.Role"},"type":"array","uniqueItems":false},"version":{"type":"string"}},"type":"object"},"body.Env":{"properties":{"name":{"maxLength":100,"minLength":1,"type":"string"},"value":{"maxLength":10000,"minLength":1,"type":"string"}},"required":["name","value"],"type":"object"},"body.GpuCapacities":{"properties":{"total":{"type":"integer"}},"type":"object"},"body.GpuGroupRead":{"properties":{"available":{"type":"integer"},"displayName":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"total":{"type":"integer"},"vendor":{"type":"string"},"zone":{"type":"string"}},"type":"object"},"body.GpuLeaseCreate":{"properties":{"gpuGroupId":{"description":"GpuGroupID is used to specify the GPU to lease.\nAs such, the lease does not specify which specific GPU to lease, but rather the type of GPU to lease.","type":"string"},"leaseForever":{"description":"LeaseForever is used to specify whether the lease should be created forever.","type":"boolean"}},"required":["gpuGroupId"],"type":"object"},"body.GpuLeaseCreated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.GpuLeaseDeleted":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.GpuLeaseRead":{"properties":{"activatedAt":{"description":"ActivatedAt specifies the time when the lease was activated. This is the time the user first attached the GPU\nor 1 day after the lease was created if the user did not attach the GPU.","type":"string"},"active":{"type":"boolean"},"assignedAt":{"description":"AssignedAt specifies the time when the lease was assigned to the user.","type":"string"},"createdAt":{"type":"string"},"expiredAt":{"type":"string"},"expiresAt":{"description":"ExpiresAt specifies the time when the lease will expire.\nThis is only present if the lease is active.","type":"string"},"gpuGroupId":{"type":"string"},"id":{"type":"string"},"leaseDuration":{"type":"number"},"queuePosition":{"type":"integer"},"userId":{"type":"string"},"vmId":{"description":"VmID is set when the lease is attached to a VM.","type":"string"}},"type":"object"},"body.GpuLeaseUpdate":{"properties":{"vmId":{"description":"VmID is used to specify the VM to attach the lease to.\n\n- If specified, the lease will be attached to the VM.\n\n- If the lease is already attached to a VM, it will be detached from the current VM and attached to the new VM.\n\n- If the lease is not active, specifying a VM will activate the lease.\n\n- If the lease is not assigned, an error will be returned.","type":"string"}},"type":"object"},"body.GpuLeaseUpdated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.GpuStatus":{"properties":{"temp":{"items":{"$ref":"#/components/schemas/body.GpuStatusTemp"},"type":"array","uniqueItems":false}},"type":"object"},"body.GpuStatusTemp":{"properties":{"main":{"type":"number"}},"type":"object"},"body.HarborWebhook":{"properties":{"event_data":{"properties":{"repository":{"properties":{"date_created":{"type":"integer"},"name":{"type":"string"},"namespace":{"type":"string"},"repo_full_name":{"type":"string"},"repo_type":{"type":"string"}},"type":"object"},"resources":{"items":{"properties":{"digest":{"type":"string"},"resource_url":{"type":"string"},"tag":{"type":"string"}},"type":"object"},"type":"array","uniqueItems":false}},"type":"object"},"occur_at":{"type":"integer"},"operator":{"type":"string"},"type":{"type":"string"}},"type":"object"},"body.HostCapacities":{"properties":{"cpuCore":{"$ref":"#/components/schemas/body.CpuCoreCapacities"},"displayName":{"type":"string"},"gpu":{"$ref":"#/components/schemas/body.GpuCapacities"},"name":{"type":"string"},"ram":{"$ref":"#/components/schemas/body.RamCapacities"},"zone":{"description":"Zone is the name of the zone where the host is located.","type":"string"}},"type":"object"},"body.HostRead":{"properties":{"displayName":{"type":"string"},"name":{"type":"string"},"zone":{"description":"Zone is the name of the zone where the host is located.","type":"string"}},"type":"object"},"body.HostStatus":{"properties":{"cpu":{"$ref":"#/components/schemas/body.CpuStatus"},"displayName":{"type":"string"},"gpu":{"$ref":"#/components/schemas/body.GpuStatus"},"name":{"type":"string"},"ram":{"$ref":"#/components/schemas/body.RamStatus"},"zone":{"description":"Zone is the name of the zone where the host is located.","type":"string"}},"type":"object"},"body.HostVerboseRead":{"properties":{"deactivatedUntil":{"type":"string"},"displayName":{"type":"string"},"enabled":{"type":"boolean"},"ip":{"type":"string"},"lastSeenAt":{"type":"string"},"name":{"type":"string"},"port":{"type":"integer"},"registeredAt":{"type":"string"},"schedulable":{"type":"boolean"},"zone":{"description":"Zone is the name of the zone where the host is located.","type":"string"}},"type":"object"},"body.HttpProxyCreate":{"properties":{"customDomain":{"description":"CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.","type":"string"},"name":{"maxLength":30,"minLength":3,"type":"string"}},"required":["name"],"type":"object"},"body.HttpProxyRead":{"properties":{"customDomain":{"$ref":"#/components/schemas/body.CustomDomainRead"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"body.HttpProxyUpdate":{"properties":{"customDomain":{"description":"CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.","type":"string"},"name":{"maxLength":30,"minLength":3,"type":"string"}},"required":["name"],"type":"object"},"body.JobRead":{"properties":{"createdAt":{"type":"string"},"finishedAt":{"type":"string"},"id":{"type":"string"},"lastError":{"type":"string"},"lastRunAt":{"type":"string"},"runAfter":{"type":"string"},"status":{"type":"string"},"type":{"type":"string"},"userId":{"type":"string"}},"type":"object"},"body.JobUpdate":{"properties":{"status":{"enum":["pending","running","failed","terminated","finished","completed"],"type":"string"}},"type":"object"},"body.K8sStats":{"properties":{"clusters":{"items":{"$ref":"#/components/schemas/body.ClusterStats"},"type":"array","uniqueItems":false},"podCount":{"type":"integer"}},"type":"object"},"body.NotificationRead":{"properties":{"completedAt":{"type":"string"},"content":{"additionalProperties":{},"type":"object"},"createdAt":{"type":"string"},"id":{"type":"string"},"readAt":{"type":"string"},"toastedAt":{"type":"string"},"type":{"type":"string"},"userId":{"type":"string"}},"type":"object"},"body.NotificationUpdate":{"properties":{"read":{"type":"boolean"},"toasted":{"type":"boolean"}},"type":"object"},"body.PortCreate":{"properties":{"httpProxy":{"$ref":"#/components/schemas/body.HttpProxyCreate"},"name":{"maxLength":100,"minLength":1,"type":"string"},"port":{"maximum":65535,"minimum":1,"type":"integer"},"protocol":{"enum":["tcp","udp"],"type":"string"}},"required":["name","port","protocol"],"type":"object"},"body.PortRead":{"properties":{"externalPort":{"type":"integer"},"httpProxy":{"$ref":"#/components/schemas/body.HttpProxyRead"},"name":{"type":"string"},"port":{"type":"integer"},"protocol":{"type":"string"}},"type":"object"},"body.PortUpdate":{"properties":{"httpProxy":{"$ref":"#/components/schemas/body.HttpProxyUpdate"},"name":{"maxLength":100,"minLength":1,"type":"string"},"port":{"maximum":65535,"minimum":1,"type":"integer"},"protocol":{"enum":["tcp","udp"],"type":"string"}},"required":["name","port","protocol"],"type":"object"},"body.PublicKey":{"properties":{"key":{"type":"string"},"name":{"maxLength":30,"minLength":1,"type":"string"}},"required":["key","name"],"type":"object"},"body.Quota":{"properties":{"cpuCores":{"type":"number"},"diskSize":{"type":"number"},"gpuLeaseDuration":{"description":"in hours","type":"number"},"ram":{"type":"number"},"snapshots":{"type":"integer"}},"type":"object"},"body.RamCapacities":{"properties":{"total":{"type":"integer"}},"type":"object"},"body.RamStatus":{"properties":{"load":{"$ref":"#/components/schemas/body.RamStatusLoad"}},"type":"object"},"body.RamStatusLoad":{"properties":{"main":{"type":"number"}},"type":"object"},"body.ReplicaStatus":{"properties":{"availableReplicas":{"description":"AvailableReplicas is the number of replicas that are available.","type":"integer"},"desiredReplicas":{"description":"DesiredReplicas is the number of replicas that the deployment should have.","type":"integer"},"readyReplicas":{"description":"ReadyReplicas is the number of replicas that are ready.","type":"integer"},"unavailableReplicas":{"description":"UnavailableReplicas is the number of replicas that are unavailable.","type":"integer"}},"type":"object"},"body.ResourceMigrationCreate":{"properties":{"resourceId":{"description":"ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.","type":"string"},"status":{"description":"Status is the status of the resource migration.\nIt is used by privileged admins to directly accept or reject a migration.\nThe field is ignored by non-admins.\n\nPossible values:\n- accepted\n- pending","type":"string"},"type":{"description":"Type is the type of the resource migration.\n\nPossible values:\n- updateOwner","enum":["updateOwner"],"type":"string"},"updateOwner":{"description":"UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is ignored if the migration type is not updateOwner.","properties":{"ownerId":{"type":"string"}},"required":["ownerId"],"type":"object"}},"required":["resourceId","type"],"type":"object"},"body.ResourceMigrationCreated":{"properties":{"createdAt":{"type":"string"},"deletedAt":{"type":"string"},"id":{"type":"string"},"jobId":{"description":"JobID is the ID of the job that was created for the resource migration.\nIt will only be set if the migration was created with status 'accepted'.","type":"string"},"resourceId":{"description":"ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.","type":"string"},"resourceType":{"description":"ResourceType is the type of the resource that is being migrated.\n\nPossible values:\n- vm\n- deployment","type":"string"},"status":{"description":"Status is the status of the resource migration.\nWhen this field is set to 'accepted', the migration will take place and then automatically be deleted.","type":"string"},"type":{"description":"Type is the type of the resource migration.\n\nPossible values:\n- updateOwner","type":"string"},"updateOwner":{"description":"UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is empty if the migration type is not updateOwner.","properties":{"ownerId":{"type":"string"}},"type":"object"},"userId":{"description":"UserID is the ID of the user who initiated the migration.","type":"string"}},"type":"object"},"body.ResourceMigrationRead":{"properties":{"createdAt":{"type":"string"},"deletedAt":{"type":"string"},"id":{"type":"string"},"resourceId":{"description":"ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.","type":"string"},"resourceType":{"description":"ResourceType is the type of the resource that is being migrated.\n\nPossible values:\n- vm\n- deployment","type":"string"},"status":{"description":"Status is the status of the resource migration.\nWhen this field is set to 'accepted', the migration will take place and then automatically be deleted.","type":"string"},"type":{"description":"Type is the type of the resource migration.\n\nPossible values:\n- updateOwner","type":"string"},"updateOwner":{"description":"UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is empty if the migration type is not updateOwner.","properties":{"ownerId":{"type":"string"}},"type":"object"},"userId":{"description":"UserID is the ID of the user who initiated the migration.","type":"string"}},"type":"object"},"body.ResourceMigrationUpdate":{"properties":{"code":{"description":"Code is a token required when accepting a migration if the acceptor is not an admin.\nIt is sent to the acceptor using the notification API","type":"string"},"status":{"description":"Status is the status of the resource migration.\nIt is used to accept a migration by setting the status to 'accepted'.\nIf the acceptor is not an admin, a Code must be provided.\n\nPossible values:\n- accepted\n- pending","type":"string"}},"required":["status"],"type":"object"},"body.ResourceMigrationUpdated":{"properties":{"createdAt":{"type":"string"},"deletedAt":{"type":"string"},"id":{"type":"string"},"jobId":{"description":"JobID is the ID of the job that was created for the resource migration.\nIt will only be set if the migration was updated with status 'accepted'.","type":"string"},"resourceId":{"description":"ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.","type":"string"},"resourceType":{"description":"ResourceType is the type of the resource that is being migrated.\n\nPossible values:\n- vm\n- deployment","type":"string"},"status":{"description":"Status is the status of the resource migration.\nWhen this field is set to 'accepted', the migration will take place and then automatically be deleted.","type":"string"},"type":{"description":"Type is the type of the resource migration.\n\nPossible values:\n- updateOwner","type":"string"},"updateOwner":{"description":"UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is empty if the migration type is not updateOwner.","properties":{"ownerId":{"type":"string"}},"type":"object"},"userId":{"description":"UserID is the ID of the user who initiated the migration.","type":"string"}},"type":"object"},"body.Role":{"properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"items":{"type":"string"},"type":"array","uniqueItems":false},"quota":{"$ref":"#/components/schemas/body.Quota"}},"type":"object"},"body.SmDeleted":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.SmRead":{"properties":{"createdAt":{"type":"string"},"id":{"type":"string"},"ownerId":{"type":"string"},"url":{"type":"string"},"zone":{"type":"string"}},"type":"object"},"body.SystemCapacities":{"properties":{"clusters":{"description":"Per Cluster","items":{"$ref":"#/components/schemas/body.ClusterCapacities"},"type":"array","uniqueItems":false},"cpuCore":{"$ref":"#/components/schemas/body.CpuCoreCapacities"},"gpu":{"$ref":"#/components/schemas/body.GpuCapacities"},"hosts":{"description":"Per Host","items":{"$ref":"#/components/schemas/body.HostCapacities"},"type":"array","uniqueItems":false},"ram":{"$ref":"#/components/schemas/body.RamCapacities"}},"type":"object"},"body.SystemStats":{"properties":{"k8s":{"$ref":"#/components/schemas/body.K8sStats"}},"type":"object"},"body.SystemStatus":{"properties":{"hosts":{"items":{"$ref":"#/components/schemas/body.HostStatus"},"type":"array","uniqueItems":false}},"type":"object"},"body.TeamCreate":{"properties":{"description":{"maxLength":1000,"type":"string"},"members":{"items":{"$ref":"#/components/schemas/body.TeamMemberCreate"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"name":{"maxLength":100,"minLength":1,"type":"string"},"resources":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false}},"required":["name"],"type":"object"},"body.TeamMember":{"properties":{"addedAt":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"gravatarUrl":{"type":"string"},"id":{"type":"string"},"joinedAt":{"type":"string"},"lastName":{"type":"string"},"memberStatus":{"type":"string"},"teamRole":{"type":"string"},"username":{"type":"string"}},"type":"object"},"body.TeamMemberCreate":{"properties":{"id":{"type":"string"},"teamRole":{"description":"default to MemberRoleAdmin right now","type":"string"}},"required":["id"],"type":"object"},"body.TeamMemberUpdate":{"properties":{"id":{"type":"string"},"teamRole":{"description":"default to MemberRoleAdmin right now","type":"string"}},"required":["id"],"type":"object"},"body.TeamRead":{"properties":{"createdAt":{"type":"string"},"description":{"type":"string"},"id":{"type":"string"},"members":{"items":{"$ref":"#/components/schemas/body.TeamMember"},"type":"array","uniqueItems":false},"name":{"type":"string"},"ownerId":{"type":"string"},"resources":{"items":{"$ref":"#/components/schemas/body.TeamResource"},"type":"array","uniqueItems":false},"updatedAt":{"type":"string"}},"type":"object"},"body.TeamResource":{"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"}},"type":"object"},"body.TeamUpdate":{"properties":{"description":{"maxLength":1000,"type":"string"},"members":{"items":{"$ref":"#/components/schemas/body.TeamMemberUpdate"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"name":{"maxLength":100,"minLength":1,"type":"string"},"resources":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false}},"type":"object"},"body.TimestampedSystemCapacities":{"properties":{"capacities":{"$ref":"#/components/schemas/body.SystemCapacities"},"timestamp":{"type":"string"}},"type":"object"},"body.TimestampedSystemStats":{"properties":{"stats":{"$ref":"#/components/schemas/body.SystemStats"},"timestamp":{"type":"string"}},"type":"object"},"body.TimestampedSystemStatus":{"properties":{"status":{"$ref":"#/components/schemas/body.SystemStatus"},"timestamp":{"type":"string"}},"type":"object"},"body.Usage":{"properties":{"cpuCores":{"type":"number"},"diskSize":{"type":"integer"},"ram":{"type":"number"}},"type":"object"},"body.UserData":{"properties":{"key":{"maxLength":255,"minLength":1,"type":"string"},"value":{"maxLength":255,"minLength":1,"type":"string"}},"required":["key","value"],"type":"object"},"body.UserRead":{"properties":{"admin":{"type":"boolean"},"apiKeys":{"items":{"$ref":"#/components/schemas/body.ApiKey"},"type":"array","uniqueItems":false},"email":{"type":"string"},"firstName":{"type":"string"},"gravatarUrl":{"type":"string"},"id":{"type":"string"},"lastName":{"type":"string"},"publicKeys":{"items":{"$ref":"#/components/schemas/body.PublicKey"},"type":"array","uniqueItems":false},"quota":{"$ref":"#/components/schemas/body.Quota"},"role":{"$ref":"#/components/schemas/body.Role"},"storageUrl":{"type":"string"},"usage":{"$ref":"#/components/schemas/body.Usage"},"userData":{"items":{"$ref":"#/components/schemas/body.UserData"},"type":"array","uniqueItems":false},"username":{"type":"string"}},"type":"object"},"body.UserUpdate":{"properties":{"apiKeys":{"description":"ApiKeys specifies the API keys that should remain. If an API key is not in this list, it will be deleted.\nHowever, API keys cannot be created, use /apiKeys endpoint to create new API keys.","items":{"$ref":"#/components/schemas/body.ApiKey"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"publicKeys":{"items":{"$ref":"#/components/schemas/body.PublicKey"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"userData":{"items":{"$ref":"#/components/schemas/body.UserData"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false}},"type":"object"},"body.VmActionCreate":{"properties":{"action":{"enum":["start","stop","restart","repair"],"type":"string"}},"required":["action"],"type":"object"},"body.VmActionCreated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.VmCreate":{"properties":{"cpuCores":{"minimum":1,"type":"integer"},"diskSize":{"minimum":10,"type":"integer"},"name":{"maxLength":30,"minLength":3,"type":"string"},"neverStale":{"type":"boolean"},"ports":{"items":{"$ref":"#/components/schemas/body.PortCreate"},"maxItems":10,"minItems":0,"type":"array","uniqueItems":false},"ram":{"minimum":1,"type":"integer"},"sshPublicKey":{"type":"string"},"zone":{"type":"string"}},"required":["cpuCores","diskSize","name","ram","sshPublicKey"],"type":"object"},"body.VmCreated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.VmDeleted":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.VmGpuLease":{"properties":{"activatedAt":{"description":"ActivatedAt specifies the time when the lease was activated. This is the time the user first attached the GPU\nor 1 day after the lease was created if the user did not attach the GPU.","type":"string"},"assignedAt":{"description":"AssignedAt specifies the time when the lease was assigned to the user.","type":"string"},"createdAt":{"type":"string"},"expiredAt":{"description":"ExpiredAt specifies the time when the lease expired.\nThis is only present if the lease is expired.","type":"string"},"expiresAt":{"description":"ExpiresAt specifies the time when the lease will expire.\nThis is only present if the lease is active.","type":"string"},"gpuGroupId":{"type":"string"},"id":{"type":"string"},"leaseDuration":{"type":"number"}},"type":"object"},"body.VmRead":{"properties":{"accessedAt":{"type":"string"},"createdAt":{"type":"string"},"gpu":{"$ref":"#/components/schemas/body.VmGpuLease"},"host":{"type":"string"},"id":{"type":"string"},"internalName":{"type":"string"},"name":{"type":"string"},"neverStale":{"type":"boolean"},"ownerId":{"type":"string"},"ports":{"items":{"$ref":"#/components/schemas/body.PortRead"},"type":"array","uniqueItems":false},"repairedAt":{"type":"string"},"specs":{"$ref":"#/components/schemas/body.VmSpecs"},"sshConnectionString":{"type":"string"},"sshPublicKey":{"type":"string"},"status":{"type":"string"},"teams":{"items":{"type":"string"},"type":"array","uniqueItems":false},"updatedAt":{"type":"string"},"zone":{"type":"string"}},"type":"object"},"body.VmSnapshotCreated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.VmSnapshotDeleted":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.VmSnapshotRead":{"properties":{"created":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"}},"type":"object"},"body.VmSpecs":{"properties":{"cpuCores":{"type":"integer"},"diskSize":{"type":"integer"},"ram":{"type":"integer"}},"type":"object"},"body.VmUpdate":{"properties":{"cpuCores":{"minimum":1,"type":"integer"},"name":{"maxLength":30,"minLength":3,"type":"string"},"neverStale":{"type":"boolean"},"ports":{"items":{"$ref":"#/components/schemas/body.PortUpdate"},"maxItems":10,"minItems":0,"type":"array","uniqueItems":false},"ram":{"minimum":1,"type":"integer"}},"type":"object"},"body.VmUpdated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.Volume":{"properties":{"appPath":{"maxLength":255,"minLength":1,"type":"string"},"name":{"maxLength":30,"minLength":3,"type":"string"},"serverPath":{"maxLength":255,"minLength":1,"type":"string"}},"required":["appPath","name","serverPath"],"type":"object"},"body.WorkerStatusRead":{"properties":{"name":{"type":"string"},"reportedAt":{"type":"string"},"status":{"type":"string"}},"type":"object"},"body.ZoneEndpoints":{"properties":{"deployment":{"type":"string"},"storage":{"type":"string"},"vm":{"type":"string"},"vmApp":{"type":"string"}},"type":"object"},"body.ZoneRead":{"properties":{"capabilities":{"items":{"type":"string"},"type":"array","uniqueItems":false},"description":{"type":"string"},"enabled":{"type":"boolean"},"endpoints":{"$ref":"#/components/schemas/body.ZoneEndpoints"},"legacy":{"type":"boolean"},"name":{"type":"string"}},"type":"object"},"sys.Error":{"properties":{"code":{"type":"string"},"msg":{"type":"string"}},"type":"object"},"sys.ErrorResponse":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/sys.Error"},"type":"array","uniqueItems":false}},"type":"object"}},"securitySchemes":{"ApiKeyAuth":{"in":"header","name":"X-Api-Key","type":"apiKey"},"KeycloakOAuth":{"flows":{"authorizationCode":{"authorizationUrl":"https://iam.cloud.cbh.kth.se/realms/cloud/protocol/openid-connect/auth","tokenUrl":"https://iam.cloud.cbh.kth.se/realms/cloud/protocol/openid-connect/token"}},"in":"header","type":"oauth2"}}}, - "info": {"contact":{"name":"Support","url":"https://github.com/kthcloud/go-deploy"},"description":"{{escape .Description}}","license":{"name":"MIT License","url":"https://github.com/kthcloud/go-deploy?tab=MIT-1-ov-file#readme"},"termsOfService":"http://swagger.io/terms/","title":"{{.Title}}","version":"{{.Version}}"}, - "externalDocs": {"description":"","url":""}, - "paths": {"/v2/deployments":{"get":{"description":"List deployments","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by user ID","in":"query","name":"userId","schema":{"type":"string"}},{"description":"Include shared","in":"query","name":"shared","schema":{"type":"boolean"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.DeploymentRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List deployments","tags":["Deployment"]},"post":{"description":"Create deployment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentCreate"}}},"description":"Deployment body","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create deployment","tags":["Deployment"]}},"/v2/deployments/{deploymentId}":{"delete":{"description":"Delete deployment","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete deployment","tags":["Deployment"]},"get":{"description":"Get deployment","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get deployment","tags":["Deployment"]},"post":{"description":"Update deployment","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentUpdate"}}},"description":"Deployment update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentUpdated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update deployment","tags":["Deployment"]}},"/v2/deployments/{deploymentId}/ciConfig":{"get":{"description":"Get CI config","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.CiConfig"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get CI config","tags":["Deployment"]}},"/v2/deployments/{deploymentId}/command":{"post":{"description":"Do command","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentCommand"}}},"description":"Command body","required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Do command","tags":["Deployment"]}},"/v2/deployments/{deploymentId}/logs":{"get":{"description":"Get logs using Server-Sent Events","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get logs using Server-Sent Events","tags":["Deployment"]}},"/v2/discover":{"get":{"description":"Discover","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DiscoverRead"}}},"description":"OK"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Discover","tags":["Discover"]}},"/v2/gpuGroups":{"get":{"description":"List GPU groups","parameters":[{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.GpuGroupRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List GPU groups","tags":["GpuGroup"]}},"/v2/gpuGroups/{gpuGroupId}":{"get":{"description":"Get GPU group","parameters":[{"description":"GPU group ID","in":"path","name":"gpuGroupId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuGroupRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get GPU group","tags":["GpuGroup"]}},"/v2/gpuLeases":{"get":{"description":"List GPU leases","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by VM ID","in":"query","name":"vmId","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.GpuLeaseRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List GPU leases","tags":["GpuLease"]},"post":{"description":"Create GPU lease","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseCreate"}}},"description":"GPU lease","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create GPU Lease","tags":["GpuLease"]}},"/v2/gpuLeases/{gpuLeaseId}":{"delete":{"description":"Delete GPU lease","parameters":[{"description":"GPU lease ID","in":"path","name":"gpuLeaseId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseDeleted"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete GPU lease","tags":["GpuLease"]},"get":{"description":"Get GPU lease","parameters":[{"description":"GPU lease ID","in":"path","name":"gpuLeaseId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get GPU lease","tags":["GpuLease"]},"post":{"description":"Update GPU lease","parameters":[{"description":"GPU lease ID","in":"path","name":"gpuLeaseId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseUpdate"}}},"description":"GPU lease","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseUpdated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update GPU lease","tags":["GpuLease"]}},"/v2/hooks/harbor":{"post":{"description":"Handle Harbor hook","parameters":[{"description":"Basic auth token","in":"header","name":"Authorization","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.HarborWebhook"}}},"description":"Harbor webhook body","required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Handle Harbor hook","tags":["Deployment"]}},"/v2/hosts":{"get":{"description":"List Hosts","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.HostRead"},"type":"array"}}},"description":"OK"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List Hosts","tags":["Host"]}},"/v2/hosts/verbose":{"get":{"description":"List Hosts verbose","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.HostVerboseRead"},"type":"array"}}},"description":"OK"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List Hosts verbose","tags":["Host"]}},"/v2/jobs":{"get":{"description":"List jobs","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by user ID","in":"query","name":"userId","schema":{"type":"string"}},{"description":"Filter by type","in":"query","name":"type","schema":{"type":"string"}},{"description":"Filter by status","in":"query","name":"status","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.JobRead"},"type":"array"}}},"description":"OK"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List jobs","tags":["Job"]}},"/v2/jobs/{jobId}":{"get":{"description":"GetJob job by id","parameters":[{"description":"Job ID","in":"path","name":"jobId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.JobRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"GetJob job by id","tags":["Job"]},"post":{"description":"Update job. Only allowed for admins.","parameters":[{"description":"Job ID","in":"path","name":"jobId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.JobUpdate"}}},"description":"Job update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.JobRead"}}},"description":"OK"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update job","tags":["Job"]}},"/v2/metrics":{"get":{"description":"Get metrics","responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Get metrics","tags":["Metrics"]}},"/v2/notifications":{"get":{"description":"List notifications","parameters":[{"description":"List all notifications","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by user ID","in":"query","name":"userId","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.NotificationRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List notifications","tags":["Notification"]}},"/v2/notifications/{notificationId}":{"delete":{"description":"Delete notification","parameters":[{"description":"Notification ID","in":"path","name":"notificationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete notification","tags":["Notification"]},"get":{"description":"Get notification","parameters":[{"description":"Notification ID","in":"path","name":"notificationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.NotificationRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get notification","tags":["Notification"]},"post":{"description":"Update notification","parameters":[{"description":"Notification ID","in":"path","name":"notificationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.NotificationUpdate"}}},"description":"Notification update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update notification","tags":["Notification"]}},"/v2/register":{"get":{"description":"Register resource","responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Register resource","tags":["Register"]}},"/v2/resourceMigrations":{"get":{"description":"List resource migrations","parameters":[{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.ResourceMigrationRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List resource migrations","tags":["ResourceMigration"]},"post":{"description":"Create resource migration","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ResourceMigrationCreate"}}},"description":"Resource Migration Create","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ResourceMigrationCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create resource migration","tags":["ResourceMigration"]}},"/v2/resourceMigrations/{resourceMigrationId}":{"delete":{"description":"Delete resource migration","parameters":[{"description":"Resource Migration ID","in":"path","name":"resourceMigrationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete resource migration","tags":["ResourceMigration"]},"get":{"description":"Get resource migration","parameters":[{"description":"Resource Migration ID","in":"path","name":"resourceMigrationId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ResourceMigrationRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get resource migration","tags":["ResourceMigration"]},"post":{"description":"Update resource migration","parameters":[{"description":"Resource Migration ID","in":"path","name":"resourceMigrationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ResourceMigrationUpdate"}}},"description":"Resource Migration Update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ResourceMigrationUpdated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update resource migration","tags":["ResourceMigration"]}},"/v2/snapshots":{"get":{"description":"List snapshots","parameters":[{"description":"Filter by VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.VmSnapshotRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List snapshots","tags":["Snapshot"]},"post":{"description":"Create snapshot","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmSnapshotCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create snapshot","tags":["Snapshot"]}},"/v2/storageManagers":{"get":{"description":"Get storage manager list","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.SmRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get storage manager list","tags":["StorageManager"]}},"/v2/storageManagers/{storageManagerId}":{"delete":{"description":"Delete storage manager","parameters":[{"description":"Storage manager ID","in":"path","name":"storageManagerId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.SmDeleted"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete storage manager","tags":["StorageManager"]},"get":{"description":"Get storage manager","parameters":[{"description":"Storage manager ID","in":"path","name":"storageManagerId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.SmDeleted"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get storage manager","tags":["StorageManager"]}},"/v2/systemCapacities":{"get":{"description":"List system capacities","parameters":[{"description":"n","in":"query","name":"n","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.TimestampedSystemCapacities"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List system capacities","tags":["System"]}},"/v2/systemStats":{"get":{"description":"List system stats","parameters":[{"description":"n","in":"query","name":"n","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.TimestampedSystemStats"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List system stats","tags":["System"]}},"/v2/systemStatus":{"get":{"description":"List system stats","parameters":[{"description":"n","in":"query","name":"n","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.TimestampedSystemStatus"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List system stats","tags":["System"]}},"/v2/teams":{"get":{"description":"List teams","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by user ID","in":"query","name":"userId","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.TeamRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List teams","tags":["Team"]},"post":{"description":"Create team","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.TeamCreate"}}},"description":"Team","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.TeamRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create team","tags":["Team"]}},"/v2/teams/{teamId}":{"delete":{"description":"Delete team","parameters":[{"description":"Team ID","in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete team","tags":["Team"]},"get":{"description":"Get team","parameters":[{"description":"Team ID","in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.TeamRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get team","tags":["Team"]},"post":{"description":"Update team","parameters":[{"description":"Team ID","in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.TeamUpdate"}}},"description":"Team","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.TeamRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update team","tags":["Team"]}},"/v2/users":{"get":{"description":"List users","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Discovery mode","in":"query","name":"discover","schema":{"type":"boolean"}},{"description":"Search query","in":"query","name":"search","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.UserRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List users","tags":["User"]}},"/v2/users/{userId}":{"get":{"description":"Get user","parameters":[{"description":"User ID","in":"path","name":"userId","required":true,"schema":{"type":"string"}},{"description":"Discovery mode","in":"query","name":"discover","schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.UserRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get user","tags":["User"]},"post":{"description":"Update user","parameters":[{"description":"User ID","in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.UserUpdate"}}},"description":"User update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.UserRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update user","tags":["User"]}},"/v2/users/{userId}/apiKeys":{"post":{"description":"Create API key","parameters":[{"description":"User ID","in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ApiKeyCreate"}}},"description":"API key create body","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ApiKeyCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create API key","tags":["User"]}},"/v2/vmActions":{"post":{"description":"Creates a new action","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmActionCreate"}}},"description":"actions body","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmActionCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Creates a new action","tags":["VmAction"]}},"/v2/vms":{"get":{"description":"List VMs","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by user ID","in":"query","name":"userId","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.VmRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List VMs","tags":["VM"]},"post":{"description":"Create VM","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmCreate"}}},"description":"VM body","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create VM","tags":["VM"]}},"/v2/vms/{vmId}":{"delete":{"description":"Delete VM","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmDeleted"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete VM","tags":["VM"]},"get":{"description":"Get VM","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get VM","tags":["VM"]},"post":{"description":"Update VM","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmUpdate"}}},"description":"VM update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmUpdated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update VM","tags":["VM"]}},"/v2/vms/{vmId}/snapshot/{snapshotId}":{"delete":{"description":"Delete snapshot","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}},{"description":"Snapshot ID","in":"path","name":"snapshotId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmSnapshotDeleted"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete snapshot","tags":["Snapshot"]},"post":{"description":"Get snapshot","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}},{"description":"Snapshot ID","in":"path","name":"snapshotId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmSnapshotRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get snapshot","tags":["Snapshot"]}},"/v2/workerStatus":{"get":{"description":"List of worker status","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.WorkerStatusRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List worker status","tags":["Status"]}},"/v2/zones":{"get":{"description":"List zones","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.ZoneRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List zones","tags":["Zone"]}}}, + "components": { + "schemas": { + "body.AllocatedGpu": { + "properties": { + "adminAccess": { + "type": "boolean" + }, + "device": { + "type": "string" + }, + "pool": { + "type": "string" + }, + "shareID": { + "type": "string" + } + }, + "type": "object" + }, + "body.ApiKey": { + "properties": { + "createdAt": { + "type": "string" + }, + "expiresAt": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "body.ApiKeyCreate": { + "properties": { + "expiresAt": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "expiresAt", + "name" + ], + "type": "object" + }, + "body.ApiKeyCreated": { + "properties": { + "createdAt": { + "type": "string" + }, + "expiresAt": { + "type": "string" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "body.BindingError": { + "properties": { + "validationErrors": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "object" + } + }, + "type": "object" + }, + "body.CiConfig": { + "properties": { + "config": { + "type": "string" + } + }, + "type": "object" + }, + "body.ClusterCapacities": { + "properties": { + "cluster": { + "type": "string" + }, + "cpuCore": { + "$ref": "#/components/schemas/body.CpuCoreCapacities" + }, + "gpu": { + "$ref": "#/components/schemas/body.GpuCapacities" + }, + "ram": { + "$ref": "#/components/schemas/body.RamCapacities" + } + }, + "type": "object" + }, + "body.ClusterStats": { + "properties": { + "cluster": { + "type": "string" + }, + "podCount": { + "type": "integer" + } + }, + "type": "object" + }, + "body.CpuCoreCapacities": { + "description": "Total", + "properties": { + "total": { + "type": "integer" + } + }, + "type": "object" + }, + "body.CpuStatus": { + "properties": { + "load": { + "$ref": "#/components/schemas/body.CpuStatusLoad" + }, + "temp": { + "$ref": "#/components/schemas/body.CpuStatusTemp" + } + }, + "type": "object" + }, + "body.CpuStatusLoad": { + "properties": { + "cores": { + "items": { + "type": "integer" + }, + "type": "array", + "uniqueItems": false + }, + "main": { + "type": "number" + }, + "max": { + "type": "number" + } + }, + "type": "object" + }, + "body.CpuStatusTemp": { + "properties": { + "cores": { + "items": { + "type": "integer" + }, + "type": "array", + "uniqueItems": false + }, + "main": { + "type": "number" + }, + "max": { + "type": "number" + } + }, + "type": "object" + }, + "body.CustomDomainRead": { + "properties": { + "domain": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "status": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "body.DeploymentCommand": { + "properties": { + "command": { + "enum": [ + "restart" + ], + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + }, + "body.DeploymentCreate": { + "properties": { + "args": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "cpuCores": { + "type": "number" + }, + "customDomain": { + "description": "CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.", + "type": "string" + }, + "envs": { + "items": { + "$ref": "#/components/schemas/body.Env" + }, + "maxItems": 1000, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "gpus": { + "items": { + "$ref": "#/components/schemas/body.DeploymentGPU" + }, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "healthCheckPath": { + "maxLength": 1000, + "minLength": 0, + "type": "string" + }, + "image": { + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + "initCommands": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "neverStale": { + "description": "Boolean to make deployment never get disabled, despite being stale", + "type": "boolean" + }, + "private": { + "description": "Deprecated: Use Visibility instead.", + "type": "boolean" + }, + "ram": { + "type": "number" + }, + "replicas": { + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + "visibility": { + "enum": [ + "public", + "private", + "auth" + ], + "type": "string" + }, + "volumes": { + "items": { + "$ref": "#/components/schemas/body.Volume" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "zone": { + "description": "Zone is the zone that the deployment will be created in.\nIf the zone is not set, the deployment will be created in the default zone.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.DeploymentCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.DeploymentGPU": { + "properties": { + "claimName": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "claimName", + "name" + ], + "type": "object" + }, + "body.DeploymentRead": { + "properties": { + "accessedAt": { + "type": "string" + }, + "args": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "createdAt": { + "type": "string" + }, + "customDomain": { + "$ref": "#/components/schemas/body.CustomDomainRead" + }, + "envs": { + "items": { + "$ref": "#/components/schemas/body.Env" + }, + "type": "array", + "uniqueItems": false + }, + "error": { + "type": "string" + }, + "healthCheckPath": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image": { + "type": "string" + }, + "initCommands": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "integrations": { + "description": "Integrations are currently not used, but could be used if we wanted to add a list of integrations to the deployment\n\nFor example GitHub", + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "internalPort": { + "type": "integer" + }, + "internalPorts": { + "items": { + "type": "integer" + }, + "type": "array", + "uniqueItems": false + }, + "name": { + "type": "string" + }, + "neverStale": { + "type": "boolean" + }, + "ownerId": { + "type": "string" + }, + "pingResult": { + "type": "integer" + }, + "private": { + "description": "Deprecated: Use Visibility instead.", + "type": "boolean" + }, + "repairedAt": { + "type": "string" + }, + "replicaStatus": { + "$ref": "#/components/schemas/body.ReplicaStatus" + }, + "restartedAt": { + "type": "string" + }, + "specs": { + "$ref": "#/components/schemas/body.DeploymentSpecs" + }, + "status": { + "type": "string" + }, + "storageUrl": { + "type": "string" + }, + "teams": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + }, + "visibility": { + "type": "string" + }, + "volumes": { + "items": { + "$ref": "#/components/schemas/body.Volume" + }, + "type": "array", + "uniqueItems": false + }, + "zone": { + "type": "string" + } + }, + "type": "object" + }, + "body.DeploymentSpecs": { + "properties": { + "cpuCores": { + "type": "number" + }, + "gpus": { + "items": { + "$ref": "#/components/schemas/body.DeploymentGPU" + }, + "type": "array", + "uniqueItems": false + }, + "ram": { + "type": "number" + }, + "replicas": { + "type": "integer" + } + }, + "type": "object" + }, + "body.DeploymentUpdate": { + "properties": { + "args": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "cpuCores": { + "type": "number" + }, + "customDomain": { + "description": "CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.", + "type": "string" + }, + "envs": { + "items": { + "$ref": "#/components/schemas/body.Env" + }, + "maxItems": 1000, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "gpus": { + "items": { + "$ref": "#/components/schemas/body.DeploymentGPU" + }, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "healthCheckPath": { + "maxLength": 1000, + "minLength": 0, + "type": "string" + }, + "image": { + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + "initCommands": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "neverStale": { + "type": "boolean" + }, + "private": { + "description": "Deprecated: Use Visibility instead.", + "type": "boolean" + }, + "ram": { + "type": "number" + }, + "replicas": { + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + "visibility": { + "enum": [ + "public", + "private", + "auth" + ], + "type": "string" + }, + "volumes": { + "items": { + "$ref": "#/components/schemas/body.Volume" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.DeploymentUpdated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.DiscoverRead": { + "properties": { + "roles": { + "items": { + "$ref": "#/components/schemas/body.Role" + }, + "type": "array", + "uniqueItems": false + }, + "version": { + "type": "string" + } + }, + "type": "object" + }, + "body.Env": { + "properties": { + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "value": { + "maxLength": 10000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "body.GpuCapacities": { + "properties": { + "total": { + "type": "integer" + } + }, + "type": "object" + }, + "body.GpuClaimConsumer": { + "properties": { + "apiGroup": { + "type": "string" + }, + "name": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "uid": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuClaimCreate": { + "properties": { + "allowedRoles": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "requested": { + "description": "Requested contains all requested GPU configurations by key (request.Name).", + "items": { + "$ref": "#/components/schemas/body.RequestedGpuCreate" + }, + "minItems": 1, + "type": "array", + "uniqueItems": false + }, + "zone": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.GpuClaimCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuClaimRead": { + "properties": { + "allocated": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/body.AllocatedGpu" + }, + "type": "array" + }, + "description": "Allocated contains the GPUs that have been successfully bound/allocated.", + "type": "object" + }, + "allowedRoles": { + "description": "Roles allowed to use this GpuClaim, empty means all", + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "consumers": { + "description": "Consumers are the workloads currently using this claim.", + "items": { + "$ref": "#/components/schemas/body.GpuClaimConsumer" + }, + "type": "array", + "uniqueItems": false + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastError": { + "description": "LastError holds the last reconciliation or provisioning error message.", + "type": "string" + }, + "name": { + "type": "string" + }, + "requested": { + "additionalProperties": { + "$ref": "#/components/schemas/body.RequestedGpu" + }, + "description": "Requested contains all requested GPU configurations by key (request.Name).", + "type": "object" + }, + "status": { + "$ref": "#/components/schemas/body.GpuClaimStatus" + }, + "updatedAt": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuClaimStatus": { + "description": "Status reflects the reconciliation and/or lifecycle state.", + "properties": { + "lastSynced": { + "type": "string" + }, + "message": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuDeviceConfigurationWrapper": { + "type": "object" + }, + "body.GpuGroupRead": { + "properties": { + "available": { + "type": "integer" + }, + "displayName": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "total": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuLeaseCreate": { + "properties": { + "gpuGroupId": { + "description": "GpuGroupID is used to specify the GPU to lease.\nAs such, the lease does not specify which specific GPU to lease, but rather the type of GPU to lease.", + "type": "string" + }, + "leaseForever": { + "description": "LeaseForever is used to specify whether the lease should be created forever.", + "type": "boolean" + } + }, + "required": [ + "gpuGroupId" + ], + "type": "object" + }, + "body.GpuLeaseCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuLeaseDeleted": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuLeaseRead": { + "properties": { + "activatedAt": { + "description": "ActivatedAt specifies the time when the lease was activated. This is the time the user first attached the GPU\nor 1 day after the lease was created if the user did not attach the GPU.", + "type": "string" + }, + "active": { + "type": "boolean" + }, + "assignedAt": { + "description": "AssignedAt specifies the time when the lease was assigned to the user.", + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "expiredAt": { + "type": "string" + }, + "expiresAt": { + "description": "ExpiresAt specifies the time when the lease will expire.\nThis is only present if the lease is active.", + "type": "string" + }, + "gpuGroupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "leaseDuration": { + "type": "number" + }, + "queuePosition": { + "type": "integer" + }, + "userId": { + "type": "string" + }, + "vmId": { + "description": "VmID is set when the lease is attached to a VM.", + "type": "string" + } + }, + "type": "object" + }, + "body.GpuLeaseUpdate": { + "properties": { + "vmId": { + "description": "VmID is used to specify the VM to attach the lease to.\n\n- If specified, the lease will be attached to the VM.\n\n- If the lease is already attached to a VM, it will be detached from the current VM and attached to the new VM.\n\n- If the lease is not active, specifying a VM will activate the lease.\n\n- If the lease is not assigned, an error will be returned.", + "type": "string" + } + }, + "type": "object" + }, + "body.GpuLeaseUpdated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuStatus": { + "properties": { + "temp": { + "items": { + "$ref": "#/components/schemas/body.GpuStatusTemp" + }, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "body.GpuStatusTemp": { + "properties": { + "main": { + "type": "number" + } + }, + "type": "object" + }, + "body.HarborWebhook": { + "properties": { + "event_data": { + "properties": { + "repository": { + "properties": { + "date_created": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "repo_full_name": { + "type": "string" + }, + "repo_type": { + "type": "string" + } + }, + "type": "object" + }, + "resources": { + "items": { + "properties": { + "digest": { + "type": "string" + }, + "resource_url": { + "type": "string" + }, + "tag": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "occur_at": { + "type": "integer" + }, + "operator": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "body.HostCapacities": { + "properties": { + "cpuCore": { + "$ref": "#/components/schemas/body.CpuCoreCapacities" + }, + "gpu": { + "$ref": "#/components/schemas/body.GpuCapacities" + }, + "ram": { + "$ref": "#/components/schemas/body.RamCapacities" + } + }, + "type": "object" + }, + "body.HostRead": { + "type": "object" + }, + "body.HostStatus": { + "properties": { + "cpu": { + "$ref": "#/components/schemas/body.CpuStatus" + }, + "gpu": { + "$ref": "#/components/schemas/body.GpuStatus" + }, + "ram": { + "$ref": "#/components/schemas/body.RamStatus" + } + }, + "type": "object" + }, + "body.HostVerboseRead": { + "properties": { + "deactivatedUntil": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "ip": { + "type": "string" + }, + "lastSeenAt": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "registeredAt": { + "type": "string" + }, + "schedulable": { + "type": "boolean" + } + }, + "type": "object" + }, + "body.HttpProxyCreate": { + "properties": { + "customDomain": { + "description": "CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.", + "type": "string" + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.HttpProxyRead": { + "properties": { + "customDomain": { + "$ref": "#/components/schemas/body.CustomDomainRead" + }, + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "body.HttpProxyUpdate": { + "properties": { + "customDomain": { + "description": "CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.", + "type": "string" + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.JobRead": { + "properties": { + "createdAt": { + "type": "string" + }, + "finishedAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastError": { + "type": "string" + }, + "lastRunAt": { + "type": "string" + }, + "runAfter": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object" + }, + "body.JobUpdate": { + "properties": { + "status": { + "enum": [ + "pending", + "running", + "failed", + "terminated", + "finished", + "completed" + ], + "type": "string" + } + }, + "type": "object" + }, + "body.K8sStats": { + "properties": { + "clusters": { + "items": { + "$ref": "#/components/schemas/body.ClusterStats" + }, + "type": "array", + "uniqueItems": false + }, + "podCount": { + "type": "integer" + } + }, + "type": "object" + }, + "body.NotificationRead": { + "properties": { + "completedAt": { + "type": "string" + }, + "content": { + "additionalProperties": {}, + "type": "object" + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "readAt": { + "type": "string" + }, + "toastedAt": { + "type": "string" + }, + "type": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object" + }, + "body.NotificationUpdate": { + "properties": { + "read": { + "type": "boolean" + }, + "toasted": { + "type": "boolean" + } + }, + "type": "object" + }, + "body.PortCreate": { + "properties": { + "httpProxy": { + "$ref": "#/components/schemas/body.HttpProxyCreate" + }, + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "port": { + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "protocol": { + "enum": [ + "tcp", + "udp" + ], + "type": "string" + } + }, + "required": [ + "name", + "port", + "protocol" + ], + "type": "object" + }, + "body.PortRead": { + "properties": { + "externalPort": { + "type": "integer" + }, + "httpProxy": { + "$ref": "#/components/schemas/body.HttpProxyRead" + }, + "name": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "protocol": { + "type": "string" + } + }, + "type": "object" + }, + "body.PortUpdate": { + "properties": { + "httpProxy": { + "$ref": "#/components/schemas/body.HttpProxyUpdate" + }, + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "port": { + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "protocol": { + "enum": [ + "tcp", + "udp" + ], + "type": "string" + } + }, + "required": [ + "name", + "port", + "protocol" + ], + "type": "object" + }, + "body.PublicKey": { + "properties": { + "key": { + "type": "string" + }, + "name": { + "maxLength": 30, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "body.Quota": { + "properties": { + "cpuCores": { + "type": "number" + }, + "diskSize": { + "type": "number" + }, + "gpuLeaseDuration": { + "description": "in hours", + "type": "number" + }, + "gpus": { + "type": "integer" + }, + "ram": { + "type": "number" + }, + "snapshots": { + "type": "integer" + } + }, + "type": "object" + }, + "body.RamCapacities": { + "properties": { + "total": { + "type": "integer" + } + }, + "type": "object" + }, + "body.RamStatus": { + "properties": { + "load": { + "$ref": "#/components/schemas/body.RamStatusLoad" + } + }, + "type": "object" + }, + "body.RamStatusLoad": { + "properties": { + "main": { + "type": "number" + } + }, + "type": "object" + }, + "body.ReplicaStatus": { + "properties": { + "availableReplicas": { + "description": "AvailableReplicas is the number of replicas that are available.", + "type": "integer" + }, + "desiredReplicas": { + "description": "DesiredReplicas is the number of replicas that the deployment should have.", + "type": "integer" + }, + "readyReplicas": { + "description": "ReadyReplicas is the number of replicas that are ready.", + "type": "integer" + }, + "unavailableReplicas": { + "description": "UnavailableReplicas is the number of replicas that are unavailable.", + "type": "integer" + } + }, + "type": "object" + }, + "body.RequestedGpu": { + "properties": { + "allocationMode": { + "enum": [ + "All", + "ExactCount" + ], + "type": "string" + }, + "capacity": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "config": { + "$ref": "#/components/schemas/body.GpuDeviceConfigurationWrapper" + }, + "count": { + "type": "integer" + }, + "deviceClassName": { + "type": "string" + }, + "selectors": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + } + }, + "required": [ + "allocationMode", + "deviceClassName" + ], + "type": "object" + }, + "body.RequestedGpuCreate": { + "properties": { + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.ResourceMigrationCreate": { + "properties": { + "resourceId": { + "description": "ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.", + "type": "string" + }, + "status": { + "description": "Status is the status of the resource migration.\nIt is used by privileged admins to directly accept or reject a migration.\nThe field is ignored by non-admins.\n\nPossible values:\n- accepted\n- pending", + "type": "string" + }, + "type": { + "description": "Type is the type of the resource migration.\n\nPossible values:\n- updateOwner", + "enum": [ + "updateOwner" + ], + "type": "string" + }, + "updateOwner": { + "description": "UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is ignored if the migration type is not updateOwner.", + "properties": { + "ownerId": { + "type": "string" + } + }, + "required": [ + "ownerId" + ], + "type": "object" + } + }, + "required": [ + "resourceId", + "type" + ], + "type": "object" + }, + "body.ResourceMigrationCreated": { + "properties": { + "jobId": { + "description": "JobID is the ID of the job that was created for the resource migration.\nIt will only be set if the migration was created with status 'accepted'.", + "type": "string" + } + }, + "type": "object" + }, + "body.ResourceMigrationRead": { + "properties": { + "createdAt": { + "type": "string" + }, + "deletedAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "resourceId": { + "description": "ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.", + "type": "string" + }, + "resourceType": { + "description": "ResourceType is the type of the resource that is being migrated.\n\nPossible values:\n- vm\n- deployment", + "type": "string" + }, + "status": { + "description": "Status is the status of the resource migration.\nWhen this field is set to 'accepted', the migration will take place and then automatically be deleted.", + "type": "string" + }, + "type": { + "description": "Type is the type of the resource migration.\n\nPossible values:\n- updateOwner", + "type": "string" + }, + "updateOwner": { + "description": "UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is empty if the migration type is not updateOwner.", + "properties": { + "ownerId": { + "type": "string" + } + }, + "type": "object" + }, + "userId": { + "description": "UserID is the ID of the user who initiated the migration.", + "type": "string" + } + }, + "type": "object" + }, + "body.ResourceMigrationUpdate": { + "properties": { + "code": { + "description": "Code is a token required when accepting a migration if the acceptor is not an admin.\nIt is sent to the acceptor using the notification API", + "type": "string" + }, + "status": { + "description": "Status is the status of the resource migration.\nIt is used to accept a migration by setting the status to 'accepted'.\nIf the acceptor is not an admin, a Code must be provided.\n\nPossible values:\n- accepted\n- pending", + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "body.ResourceMigrationUpdated": { + "properties": { + "jobId": { + "description": "JobID is the ID of the job that was created for the resource migration.\nIt will only be set if the migration was updated with status 'accepted'.", + "type": "string" + } + }, + "type": "object" + }, + "body.Role": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "permissions": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "quota": { + "$ref": "#/components/schemas/body.Quota" + } + }, + "type": "object" + }, + "body.SmDeleted": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.SmRead": { + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "url": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "type": "object" + }, + "body.SystemCapacities": { + "properties": { + "clusters": { + "description": "Per Cluster", + "items": { + "$ref": "#/components/schemas/body.ClusterCapacities" + }, + "type": "array", + "uniqueItems": false + }, + "cpuCore": { + "$ref": "#/components/schemas/body.CpuCoreCapacities" + }, + "gpu": { + "$ref": "#/components/schemas/body.GpuCapacities" + }, + "hosts": { + "description": "Per Host", + "items": { + "$ref": "#/components/schemas/body.HostCapacities" + }, + "type": "array", + "uniqueItems": false + }, + "ram": { + "$ref": "#/components/schemas/body.RamCapacities" + } + }, + "type": "object" + }, + "body.SystemStats": { + "properties": { + "k8s": { + "$ref": "#/components/schemas/body.K8sStats" + } + }, + "type": "object" + }, + "body.SystemStatus": { + "properties": { + "hosts": { + "items": { + "$ref": "#/components/schemas/body.HostStatus" + }, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "body.TeamCreate": { + "properties": { + "description": { + "maxLength": 1000, + "type": "string" + }, + "members": { + "items": { + "$ref": "#/components/schemas/body.TeamMemberCreate" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "resources": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.TeamMember": { + "properties": { + "addedAt": { + "type": "string" + }, + "joinedAt": { + "type": "string" + }, + "memberStatus": { + "type": "string" + }, + "teamRole": { + "type": "string" + } + }, + "type": "object" + }, + "body.TeamMemberCreate": { + "properties": { + "id": { + "type": "string" + }, + "teamRole": { + "description": "default to MemberRoleAdmin right now", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "body.TeamMemberUpdate": { + "properties": { + "id": { + "type": "string" + }, + "teamRole": { + "description": "default to MemberRoleAdmin right now", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "body.TeamRead": { + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "members": { + "items": { + "$ref": "#/components/schemas/body.TeamMember" + }, + "type": "array", + "uniqueItems": false + }, + "name": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "resources": { + "items": { + "$ref": "#/components/schemas/body.TeamResource" + }, + "type": "array", + "uniqueItems": false + }, + "updatedAt": { + "type": "string" + } + }, + "type": "object" + }, + "body.TeamResource": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "body.TeamUpdate": { + "properties": { + "description": { + "maxLength": 1000, + "type": "string" + }, + "members": { + "items": { + "$ref": "#/components/schemas/body.TeamMemberUpdate" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "resources": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "body.TimestampedSystemCapacities": { + "properties": { + "capacities": { + "$ref": "#/components/schemas/body.SystemCapacities" + }, + "timestamp": { + "type": "string" + } + }, + "type": "object" + }, + "body.TimestampedSystemStats": { + "properties": { + "stats": { + "$ref": "#/components/schemas/body.SystemStats" + }, + "timestamp": { + "type": "string" + } + }, + "type": "object" + }, + "body.TimestampedSystemStatus": { + "properties": { + "status": { + "$ref": "#/components/schemas/body.SystemStatus" + }, + "timestamp": { + "type": "string" + } + }, + "type": "object" + }, + "body.Usage": { + "properties": { + "cpuCores": { + "type": "number" + }, + "diskSize": { + "type": "integer" + }, + "gpus": { + "type": "integer" + }, + "ram": { + "type": "number" + } + }, + "type": "object" + }, + "body.UserData": { + "properties": { + "key": { + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "value": { + "maxLength": 255, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, + "body.UserRead": { + "properties": { + "admin": { + "type": "boolean" + }, + "apiKeys": { + "items": { + "$ref": "#/components/schemas/body.ApiKey" + }, + "type": "array", + "uniqueItems": false + }, + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "gravatarUrl": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "publicKeys": { + "items": { + "$ref": "#/components/schemas/body.PublicKey" + }, + "type": "array", + "uniqueItems": false + }, + "quota": { + "$ref": "#/components/schemas/body.Quota" + }, + "role": { + "$ref": "#/components/schemas/body.Role" + }, + "storageUrl": { + "type": "string" + }, + "usage": { + "$ref": "#/components/schemas/body.Usage" + }, + "userData": { + "items": { + "$ref": "#/components/schemas/body.UserData" + }, + "type": "array", + "uniqueItems": false + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "body.UserUpdate": { + "properties": { + "apiKeys": { + "description": "ApiKeys specifies the API keys that should remain. If an API key is not in this list, it will be deleted.\nHowever, API keys cannot be created, use /apiKeys endpoint to create new API keys.", + "items": { + "$ref": "#/components/schemas/body.ApiKey" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "publicKeys": { + "items": { + "$ref": "#/components/schemas/body.PublicKey" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "userData": { + "items": { + "$ref": "#/components/schemas/body.UserData" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "body.VmActionCreate": { + "properties": { + "action": { + "enum": [ + "start", + "stop", + "restart", + "repair" + ], + "type": "string" + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "body.VmActionCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmCreate": { + "properties": { + "cpuCores": { + "minimum": 1, + "type": "integer" + }, + "diskSize": { + "minimum": 10, + "type": "integer" + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "neverStale": { + "type": "boolean" + }, + "ports": { + "items": { + "$ref": "#/components/schemas/body.PortCreate" + }, + "maxItems": 10, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "ram": { + "minimum": 1, + "type": "integer" + }, + "sshPublicKey": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "required": [ + "cpuCores", + "diskSize", + "name", + "ram", + "sshPublicKey" + ], + "type": "object" + }, + "body.VmCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmDeleted": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmGpuLease": { + "properties": { + "activatedAt": { + "description": "ActivatedAt specifies the time when the lease was activated. This is the time the user first attached the GPU\nor 1 day after the lease was created if the user did not attach the GPU.", + "type": "string" + }, + "assignedAt": { + "description": "AssignedAt specifies the time when the lease was assigned to the user.", + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "expiredAt": { + "description": "ExpiredAt specifies the time when the lease expired.\nThis is only present if the lease is expired.", + "type": "string" + }, + "expiresAt": { + "description": "ExpiresAt specifies the time when the lease will expire.\nThis is only present if the lease is active.", + "type": "string" + }, + "gpuGroupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "leaseDuration": { + "type": "number" + } + }, + "type": "object" + }, + "body.VmRead": { + "properties": { + "accessedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "gpu": { + "$ref": "#/components/schemas/body.VmGpuLease" + }, + "host": { + "type": "string" + }, + "id": { + "type": "string" + }, + "internalName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "neverStale": { + "type": "boolean" + }, + "ownerId": { + "type": "string" + }, + "ports": { + "items": { + "$ref": "#/components/schemas/body.PortRead" + }, + "type": "array", + "uniqueItems": false + }, + "repairedAt": { + "type": "string" + }, + "specs": { + "$ref": "#/components/schemas/body.VmSpecs" + }, + "sshConnectionString": { + "type": "string" + }, + "sshPublicKey": { + "type": "string" + }, + "status": { + "type": "string" + }, + "teams": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "updatedAt": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmSnapshotCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmSnapshotDeleted": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmSnapshotRead": { + "properties": { + "created": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmSpecs": { + "properties": { + "cpuCores": { + "type": "integer" + }, + "diskSize": { + "type": "integer" + }, + "ram": { + "type": "integer" + } + }, + "type": "object" + }, + "body.VmUpdate": { + "properties": { + "cpuCores": { + "minimum": 1, + "type": "integer" + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "neverStale": { + "type": "boolean" + }, + "ports": { + "items": { + "$ref": "#/components/schemas/body.PortUpdate" + }, + "maxItems": 10, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "ram": { + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "body.VmUpdated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.Volume": { + "properties": { + "appPath": { + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "serverPath": { + "maxLength": 255, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "appPath", + "name", + "serverPath" + ], + "type": "object" + }, + "body.WorkerStatusRead": { + "properties": { + "name": { + "type": "string" + }, + "reportedAt": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "body.ZoneEndpoints": { + "properties": { + "deployment": { + "type": "string" + }, + "storage": { + "type": "string" + }, + "vm": { + "type": "string" + }, + "vmApp": { + "type": "string" + } + }, + "type": "object" + }, + "body.ZoneRead": { + "properties": { + "capabilities": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "endpoints": { + "$ref": "#/components/schemas/body.ZoneEndpoints" + }, + "legacy": { + "type": "boolean" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "sys.Error": { + "properties": { + "code": { + "type": "string" + }, + "msg": { + "type": "string" + } + }, + "type": "object" + }, + "sys.ErrorResponse": { + "properties": { + "errors": { + "items": { + "$ref": "#/components/schemas/sys.Error" + }, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + } + }, + "securitySchemes": { + "ApiKeyAuth": { + "in": "header", + "name": "X-Api-Key", + "type": "apiKey" + }, + "KeycloakOAuth": { + "flows": { + "authorizationCode": { + "authorizationUrl": "https://iam.cloud.cbh.kth.se/realms/cloud/protocol/openid-connect/auth", + "tokenUrl": "https://iam.cloud.cbh.kth.se/realms/cloud/protocol/openid-connect/token" + } + }, + "in": "header", + "type": "oauth2" + } + } + }, + "info": { + "contact": { + "name": "Support", + "url": "https://github.com/kthcloud/go-deploy" + }, + "description": "{{escape .Description}}", + "license": { + "name": "MIT License", + "url": "https://github.com/kthcloud/go-deploy?tab=MIT-1-ov-file#readme" + }, + "termsOfService": "http://swagger.io/terms/", + "title": "{{.Title}}", + "version": "{{.Version}}" + }, + "externalDocs": { + "description": "", + "url": "" + }, + "paths": { + "/v2/deployments": { + "get": { + "description": "List deployments", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by user ID", + "in": "query", + "name": "userId", + "schema": { + "type": "string" + } + }, + { + "description": "Include shared", + "in": "query", + "name": "shared", + "schema": { + "type": "boolean" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.DeploymentRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List deployments", + "tags": [ + "Deployment" + ] + }, + "post": { + "description": "Create deployment", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.DeploymentCreate", + "summary": "body", + "description": "Deployment body" + } + ] + } + } + }, + "description": "Deployment body", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.DeploymentRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create deployment", + "tags": [ + "Deployment" + ] + } + }, + "/v2/deployments/{deploymentId}": { + "delete": { + "description": "Delete deployment", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.DeploymentCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete deployment", + "tags": [ + "Deployment" + ] + }, + "get": { + "description": "Get deployment", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.DeploymentRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get deployment", + "tags": [ + "Deployment" + ] + }, + "post": { + "description": "Update deployment", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.DeploymentUpdate", + "summary": "body", + "description": "Deployment update" + } + ] + } + } + }, + "description": "Deployment update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.DeploymentUpdated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update deployment", + "tags": [ + "Deployment" + ] + } + }, + "/v2/deployments/{deploymentId}/ciConfig": { + "get": { + "description": "Get CI config", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.CiConfig" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get CI config", + "tags": [ + "Deployment" + ] + } + }, + "/v2/deployments/{deploymentId}/command": { + "post": { + "description": "Do command", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.DeploymentCommand", + "summary": "body", + "description": "Command body" + } + ] + } + } + }, + "description": "Command body", + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Do command", + "tags": [ + "Deployment" + ] + } + }, + "/v2/deployments/{deploymentId}/logs": { + "get": { + "description": "Get logs using Server-Sent Events", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get logs using Server-Sent Events", + "tags": [ + "Deployment" + ] + } + }, + "/v2/discover": { + "get": { + "description": "Discover", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.DiscoverRead" + } + } + }, + "description": "OK" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Discover", + "tags": [ + "Discover" + ] + } + }, + "/v2/gpuClaims": { + "get": { + "description": "List GPU claims", + "parameters": [ + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + }, + { + "description": "Admin detailed list", + "in": "query", + "name": "detailed", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.GpuClaimRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List GPU claims", + "tags": [ + "GpuClaim" + ] + }, + "post": { + "description": "Create GpuClaim", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.GpuClaimCreate", + "summary": "body", + "description": "GpuClaim body" + } + ] + } + } + }, + "description": "GpuClaim body", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuClaimCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create GpuClaim", + "tags": [ + "GpuClaim" + ] + } + }, + "/v2/gpuClaims/{gpuClaimId}": { + "delete": { + "description": "Delete GpuClaim", + "parameters": [ + { + "description": "GpuClaim ID", + "in": "path", + "name": "gpuClaimId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuClaimCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete GpuClaim", + "tags": [ + "GpuClaim" + ] + }, + "get": { + "description": "Get GPU claim", + "parameters": [ + { + "description": "GPU claim ID", + "in": "path", + "name": "gpuClaimId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuClaimRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get GPU claim", + "tags": [ + "GpuClaim" + ] + } + }, + "/v2/gpuGroups": { + "get": { + "description": "List GPU groups", + "parameters": [ + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.GpuGroupRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List GPU groups", + "tags": [ + "GpuGroup" + ] + } + }, + "/v2/gpuGroups/{gpuGroupId}": { + "get": { + "description": "Get GPU group", + "parameters": [ + { + "description": "GPU group ID", + "in": "path", + "name": "gpuGroupId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuGroupRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get GPU group", + "tags": [ + "GpuGroup" + ] + } + }, + "/v2/gpuLeases": { + "get": { + "description": "List GPU leases", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by VM ID", + "in": "query", + "name": "vmId", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.GpuLeaseRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List GPU leases", + "tags": [ + "GpuLease" + ] + }, + "post": { + "description": "Create GPU lease", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.GpuLeaseCreate", + "summary": "body", + "description": "GPU lease" + } + ] + } + } + }, + "description": "GPU lease", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuLeaseCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create GPU Lease", + "tags": [ + "GpuLease" + ] + } + }, + "/v2/gpuLeases/{gpuLeaseId}": { + "delete": { + "description": "Delete GPU lease", + "parameters": [ + { + "description": "GPU lease ID", + "in": "path", + "name": "gpuLeaseId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuLeaseDeleted" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete GPU lease", + "tags": [ + "GpuLease" + ] + }, + "get": { + "description": "Get GPU lease", + "parameters": [ + { + "description": "GPU lease ID", + "in": "path", + "name": "gpuLeaseId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuLeaseRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get GPU lease", + "tags": [ + "GpuLease" + ] + }, + "post": { + "description": "Update GPU lease", + "parameters": [ + { + "description": "GPU lease ID", + "in": "path", + "name": "gpuLeaseId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.GpuLeaseUpdate", + "summary": "body", + "description": "GPU lease" + } + ] + } + } + }, + "description": "GPU lease", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuLeaseUpdated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update GPU lease", + "tags": [ + "GpuLease" + ] + } + }, + "/v2/hooks/harbor": { + "post": { + "description": "Handle Harbor hook", + "parameters": [ + { + "description": "Basic auth token", + "in": "header", + "name": "Authorization", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.HarborWebhook", + "summary": "body", + "description": "Harbor webhook body" + } + ] + } + } + }, + "description": "Harbor webhook body", + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Handle Harbor hook", + "tags": [ + "Deployment" + ] + } + }, + "/v2/hosts": { + "get": { + "description": "List Hosts", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.HostRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List Hosts", + "tags": [ + "Host" + ] + } + }, + "/v2/hosts/verbose": { + "get": { + "description": "List Hosts verbose", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.HostVerboseRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List Hosts verbose", + "tags": [ + "Host" + ] + } + }, + "/v2/jobs": { + "get": { + "description": "List jobs", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by user ID", + "in": "query", + "name": "userId", + "schema": { + "type": "string" + } + }, + { + "description": "Filter by type", + "in": "query", + "name": "type", + "schema": { + "type": "string" + } + }, + { + "description": "Filter by status", + "in": "query", + "name": "status", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.JobRead" + }, + "type": "array" + } + } + }, + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List jobs", + "tags": [ + "Job" + ] + } + }, + "/v2/jobs/{jobId}": { + "get": { + "description": "GetJob job by id", + "parameters": [ + { + "description": "Job ID", + "in": "path", + "name": "jobId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.JobRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "GetJob job by id", + "tags": [ + "Job" + ] + }, + "post": { + "description": "Update job. Only allowed for admins.", + "parameters": [ + { + "description": "Job ID", + "in": "path", + "name": "jobId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.JobUpdate", + "summary": "body", + "description": "Job update" + } + ] + } + } + }, + "description": "Job update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.JobRead" + } + } + }, + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update job", + "tags": [ + "Job" + ] + } + }, + "/v2/metrics": { + "get": { + "description": "Get metrics", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "description": "OK" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Get metrics", + "tags": [ + "Metrics" + ] + } + }, + "/v2/notifications": { + "get": { + "description": "List notifications", + "parameters": [ + { + "description": "List all notifications", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by user ID", + "in": "query", + "name": "userId", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.NotificationRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List notifications", + "tags": [ + "Notification" + ] + } + }, + "/v2/notifications/{notificationId}": { + "delete": { + "description": "Delete notification", + "parameters": [ + { + "description": "Notification ID", + "in": "path", + "name": "notificationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete notification", + "tags": [ + "Notification" + ] + }, + "get": { + "description": "Get notification", + "parameters": [ + { + "description": "Notification ID", + "in": "path", + "name": "notificationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.NotificationRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get notification", + "tags": [ + "Notification" + ] + }, + "post": { + "description": "Update notification", + "parameters": [ + { + "description": "Notification ID", + "in": "path", + "name": "notificationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.NotificationUpdate", + "summary": "body", + "description": "Notification update" + } + ] + } + } + }, + "description": "Notification update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update notification", + "tags": [ + "Notification" + ] + } + }, + "/v2/register": { + "get": { + "description": "Register resource", + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Register resource", + "tags": [ + "Register" + ] + } + }, + "/v2/resourceMigrations": { + "get": { + "description": "List resource migrations", + "parameters": [ + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.ResourceMigrationRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List resource migrations", + "tags": [ + "ResourceMigration" + ] + }, + "post": { + "description": "Create resource migration", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.ResourceMigrationCreate", + "summary": "body", + "description": "Resource Migration Create" + } + ] + } + } + }, + "description": "Resource Migration Create", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.ResourceMigrationCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create resource migration", + "tags": [ + "ResourceMigration" + ] + } + }, + "/v2/resourceMigrations/{resourceMigrationId}": { + "delete": { + "description": "Delete resource migration", + "parameters": [ + { + "description": "Resource Migration ID", + "in": "path", + "name": "resourceMigrationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete resource migration", + "tags": [ + "ResourceMigration" + ] + }, + "get": { + "description": "Get resource migration", + "parameters": [ + { + "description": "Resource Migration ID", + "in": "path", + "name": "resourceMigrationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.ResourceMigrationRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get resource migration", + "tags": [ + "ResourceMigration" + ] + }, + "post": { + "description": "Update resource migration", + "parameters": [ + { + "description": "Resource Migration ID", + "in": "path", + "name": "resourceMigrationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.ResourceMigrationUpdate", + "summary": "body", + "description": "Resource Migration Update" + } + ] + } + } + }, + "description": "Resource Migration Update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.ResourceMigrationUpdated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update resource migration", + "tags": [ + "ResourceMigration" + ] + } + }, + "/v2/snapshots/{vmId}": { + "get": { + "description": "List snapshots", + "parameters": [ + { + "description": "Filter by VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.VmSnapshotRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List snapshots", + "tags": [ + "Snapshot" + ] + }, + "post": { + "description": "Create snapshot", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmSnapshotCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create snapshot", + "tags": [ + "Snapshot" + ] + } + }, + "/v2/storageManagers": { + "get": { + "description": "Get storage manager list", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.SmRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get storage manager list", + "tags": [ + "StorageManager" + ] + } + }, + "/v2/storageManagers/{storageManagerId}": { + "delete": { + "description": "Delete storage manager", + "parameters": [ + { + "description": "Storage manager ID", + "in": "path", + "name": "storageManagerId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.SmDeleted" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete storage manager", + "tags": [ + "StorageManager" + ] + }, + "get": { + "description": "Get storage manager", + "parameters": [ + { + "description": "Storage manager ID", + "in": "path", + "name": "storageManagerId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.SmDeleted" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get storage manager", + "tags": [ + "StorageManager" + ] + } + }, + "/v2/systemCapacities": { + "get": { + "description": "List system capacities", + "parameters": [ + { + "description": "n", + "in": "query", + "name": "n", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.TimestampedSystemCapacities" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List system capacities", + "tags": [ + "System" + ] + } + }, + "/v2/systemStats": { + "get": { + "description": "List system stats", + "parameters": [ + { + "description": "n", + "in": "query", + "name": "n", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.TimestampedSystemStats" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List system stats", + "tags": [ + "System" + ] + } + }, + "/v2/systemStatus": { + "get": { + "description": "List system stats", + "parameters": [ + { + "description": "n", + "in": "query", + "name": "n", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.TimestampedSystemStatus" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List system stats", + "tags": [ + "System" + ] + } + }, + "/v2/teams": { + "get": { + "description": "List teams", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by user ID", + "in": "query", + "name": "userId", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.TeamRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List teams", + "tags": [ + "Team" + ] + }, + "post": { + "description": "Create team", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.TeamCreate", + "summary": "body", + "description": "Team" + } + ] + } + } + }, + "description": "Team", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.TeamRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create team", + "tags": [ + "Team" + ] + } + }, + "/v2/teams/{teamId}": { + "delete": { + "description": "Delete team", + "parameters": [ + { + "description": "Team ID", + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete team", + "tags": [ + "Team" + ] + }, + "get": { + "description": "Get team", + "parameters": [ + { + "description": "Team ID", + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.TeamRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get team", + "tags": [ + "Team" + ] + }, + "post": { + "description": "Update team", + "parameters": [ + { + "description": "Team ID", + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.TeamUpdate", + "summary": "body", + "description": "Team" + } + ] + } + } + }, + "description": "Team", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.TeamRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update team", + "tags": [ + "Team" + ] + } + }, + "/v2/users": { + "get": { + "description": "List users", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Discovery mode", + "in": "query", + "name": "discover", + "schema": { + "type": "boolean" + } + }, + { + "description": "Search query", + "in": "query", + "name": "search", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.UserRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List users", + "tags": [ + "User" + ] + } + }, + "/v2/users/{userId}": { + "get": { + "description": "Get user", + "parameters": [ + { + "description": "User ID", + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Discovery mode", + "in": "query", + "name": "discover", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.UserRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get user", + "tags": [ + "User" + ] + }, + "post": { + "description": "Update user", + "parameters": [ + { + "description": "User ID", + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.UserUpdate", + "summary": "body", + "description": "User update" + } + ] + } + } + }, + "description": "User update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.UserRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update user", + "tags": [ + "User" + ] + } + }, + "/v2/users/{userId}/apiKeys": { + "post": { + "description": "Create API key", + "parameters": [ + { + "description": "User ID", + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.ApiKeyCreate", + "summary": "body", + "description": "API key create body" + } + ] + } + } + }, + "description": "API key create body", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.ApiKeyCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create API key", + "tags": [ + "User" + ] + } + }, + "/v2/vmActions/{vmId}": { + "post": { + "description": "Creates a new action", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.VmActionCreate", + "summary": "body", + "description": "actions body" + } + ] + } + } + }, + "description": "actions body", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmActionCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Creates a new action", + "tags": [ + "VmAction" + ] + } + }, + "/v2/vms": { + "get": { + "description": "List VMs", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by user ID", + "in": "query", + "name": "userId", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.VmRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List VMs", + "tags": [ + "VM" + ] + }, + "post": { + "description": "Create VM", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.VmCreate", + "summary": "body", + "description": "VM body" + } + ] + } + } + }, + "description": "VM body", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create VM", + "tags": [ + "VM" + ] + } + }, + "/v2/vms/{vmId}": { + "delete": { + "description": "Delete VM", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmDeleted" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete VM", + "tags": [ + "VM" + ] + }, + "get": { + "description": "Get VM", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get VM", + "tags": [ + "VM" + ] + }, + "post": { + "description": "Update VM", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.VmUpdate", + "summary": "body", + "description": "VM update" + } + ] + } + } + }, + "description": "VM update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmUpdated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update VM", + "tags": [ + "VM" + ] + } + }, + "/v2/vms/{vmId}/snapshot/{snapshotId}": { + "delete": { + "description": "Delete snapshot", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Snapshot ID", + "in": "path", + "name": "snapshotId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmSnapshotDeleted" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete snapshot", + "tags": [ + "Snapshot" + ] + }, + "post": { + "description": "Get snapshot", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Snapshot ID", + "in": "path", + "name": "snapshotId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmSnapshotRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get snapshot", + "tags": [ + "Snapshot" + ] + } + }, + "/v2/workerStatus": { + "get": { + "description": "List of worker status", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.WorkerStatusRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List worker status", + "tags": [ + "Status" + ] + } + }, + "/v2/zones": { + "get": { + "description": "List zones", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.ZoneRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List zones", + "tags": [ + "Zone" + ] + } + } + }, "openapi": "3.1.0" }` diff --git a/docs/api/v2/V2_swagger.json b/docs/api/v2/V2_swagger.json index d9eef331..a5c2abc3 100644 --- a/docs/api/v2/V2_swagger.json +++ b/docs/api/v2/V2_swagger.json @@ -1,7 +1,7243 @@ { - "components": {"schemas":{"body.ApiKey":{"properties":{"createdAt":{"type":"string"},"expiresAt":{"type":"string"},"name":{"type":"string"}},"type":"object"},"body.ApiKeyCreate":{"properties":{"expiresAt":{"type":"string"},"name":{"type":"string"}},"required":["expiresAt","name"],"type":"object"},"body.ApiKeyCreated":{"properties":{"createdAt":{"type":"string"},"expiresAt":{"type":"string"},"key":{"type":"string"},"name":{"type":"string"}},"type":"object"},"body.BindingError":{"properties":{"validationErrors":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"}},"type":"object"},"body.CiConfig":{"properties":{"config":{"type":"string"}},"type":"object"},"body.ClusterCapacities":{"properties":{"cluster":{"type":"string"},"cpuCore":{"$ref":"#/components/schemas/body.CpuCoreCapacities"},"gpu":{"$ref":"#/components/schemas/body.GpuCapacities"},"ram":{"$ref":"#/components/schemas/body.RamCapacities"}},"type":"object"},"body.ClusterStats":{"properties":{"cluster":{"type":"string"},"podCount":{"type":"integer"}},"type":"object"},"body.CpuCoreCapacities":{"description":"Total","properties":{"total":{"type":"integer"}},"type":"object"},"body.CpuStatus":{"properties":{"load":{"$ref":"#/components/schemas/body.CpuStatusLoad"},"temp":{"$ref":"#/components/schemas/body.CpuStatusTemp"}},"type":"object"},"body.CpuStatusLoad":{"properties":{"cores":{"items":{"type":"integer"},"type":"array","uniqueItems":false},"main":{"type":"number"},"max":{"type":"number"}},"type":"object"},"body.CpuStatusTemp":{"properties":{"cores":{"items":{"type":"integer"},"type":"array","uniqueItems":false},"main":{"type":"number"},"max":{"type":"number"}},"type":"object"},"body.CustomDomainRead":{"properties":{"domain":{"type":"string"},"secret":{"type":"string"},"status":{"type":"string"},"url":{"type":"string"}},"type":"object"},"body.DeploymentCommand":{"properties":{"command":{"enum":["restart"],"type":"string"}},"required":["command"],"type":"object"},"body.DeploymentCreate":{"properties":{"args":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"cpuCores":{"type":"number"},"customDomain":{"description":"CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.","type":"string"},"envs":{"items":{"$ref":"#/components/schemas/body.Env"},"maxItems":1000,"minItems":0,"type":"array","uniqueItems":false},"healthCheckPath":{"maxLength":1000,"minLength":0,"type":"string"},"image":{"maxLength":1000,"minLength":1,"type":"string"},"initCommands":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"name":{"maxLength":30,"minLength":3,"type":"string"},"neverStale":{"description":"Boolean to make deployment never get disabled, despite being stale","type":"boolean"},"private":{"description":"Deprecated: Use Visibility instead.","type":"boolean"},"ram":{"type":"number"},"replicas":{"maximum":100,"minimum":0,"type":"integer"},"visibility":{"enum":["public","private","auth"],"type":"string"},"volumes":{"items":{"$ref":"#/components/schemas/body.Volume"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"zone":{"description":"Zone is the zone that the deployment will be created in.\nIf the zone is not set, the deployment will be created in the default zone.","type":"string"}},"required":["name"],"type":"object"},"body.DeploymentCreated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.DeploymentRead":{"properties":{"accessedAt":{"type":"string"},"args":{"items":{"type":"string"},"type":"array","uniqueItems":false},"createdAt":{"type":"string"},"customDomain":{"$ref":"#/components/schemas/body.CustomDomainRead"},"envs":{"items":{"$ref":"#/components/schemas/body.Env"},"type":"array","uniqueItems":false},"error":{"type":"string"},"healthCheckPath":{"type":"string"},"id":{"type":"string"},"image":{"type":"string"},"initCommands":{"items":{"type":"string"},"type":"array","uniqueItems":false},"integrations":{"description":"Integrations are currently not used, but could be used if we wanted to add a list of integrations to the deployment\n\nFor example GitHub","items":{"type":"string"},"type":"array","uniqueItems":false},"internalPort":{"type":"integer"},"internalPorts":{"items":{"type":"integer"},"type":"array","uniqueItems":false},"name":{"type":"string"},"neverStale":{"type":"boolean"},"ownerId":{"type":"string"},"pingResult":{"type":"integer"},"private":{"description":"Deprecated: Use Visibility instead.","type":"boolean"},"repairedAt":{"type":"string"},"replicaStatus":{"$ref":"#/components/schemas/body.ReplicaStatus"},"restartedAt":{"type":"string"},"specs":{"$ref":"#/components/schemas/body.DeploymentSpecs"},"status":{"type":"string"},"storageUrl":{"type":"string"},"teams":{"items":{"type":"string"},"type":"array","uniqueItems":false},"type":{"type":"string"},"updatedAt":{"type":"string"},"url":{"type":"string"},"visibility":{"type":"string"},"volumes":{"items":{"$ref":"#/components/schemas/body.Volume"},"type":"array","uniqueItems":false},"zone":{"type":"string"}},"type":"object"},"body.DeploymentSpecs":{"properties":{"cpuCores":{"type":"number"},"ram":{"type":"number"},"replicas":{"type":"integer"}},"type":"object"},"body.DeploymentUpdate":{"properties":{"args":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"cpuCores":{"type":"number"},"customDomain":{"description":"CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.","type":"string"},"envs":{"items":{"$ref":"#/components/schemas/body.Env"},"maxItems":1000,"minItems":0,"type":"array","uniqueItems":false},"healthCheckPath":{"maxLength":1000,"minLength":0,"type":"string"},"image":{"maxLength":1000,"minLength":1,"type":"string"},"initCommands":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"name":{"maxLength":30,"minLength":3,"type":"string"},"neverStale":{"type":"boolean"},"private":{"description":"Deprecated: Use Visibility instead.","type":"boolean"},"ram":{"type":"number"},"replicas":{"maximum":100,"minimum":0,"type":"integer"},"visibility":{"enum":["public","private","auth"],"type":"string"},"volumes":{"items":{"$ref":"#/components/schemas/body.Volume"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false}},"required":["name"],"type":"object"},"body.DeploymentUpdated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.DiscoverRead":{"properties":{"roles":{"items":{"$ref":"#/components/schemas/body.Role"},"type":"array","uniqueItems":false},"version":{"type":"string"}},"type":"object"},"body.Env":{"properties":{"name":{"maxLength":100,"minLength":1,"type":"string"},"value":{"maxLength":10000,"minLength":1,"type":"string"}},"required":["name","value"],"type":"object"},"body.GpuCapacities":{"properties":{"total":{"type":"integer"}},"type":"object"},"body.GpuGroupRead":{"properties":{"available":{"type":"integer"},"displayName":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"total":{"type":"integer"},"vendor":{"type":"string"},"zone":{"type":"string"}},"type":"object"},"body.GpuLeaseCreate":{"properties":{"gpuGroupId":{"description":"GpuGroupID is used to specify the GPU to lease.\nAs such, the lease does not specify which specific GPU to lease, but rather the type of GPU to lease.","type":"string"},"leaseForever":{"description":"LeaseForever is used to specify whether the lease should be created forever.","type":"boolean"}},"required":["gpuGroupId"],"type":"object"},"body.GpuLeaseCreated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.GpuLeaseDeleted":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.GpuLeaseRead":{"properties":{"activatedAt":{"description":"ActivatedAt specifies the time when the lease was activated. This is the time the user first attached the GPU\nor 1 day after the lease was created if the user did not attach the GPU.","type":"string"},"active":{"type":"boolean"},"assignedAt":{"description":"AssignedAt specifies the time when the lease was assigned to the user.","type":"string"},"createdAt":{"type":"string"},"expiredAt":{"type":"string"},"expiresAt":{"description":"ExpiresAt specifies the time when the lease will expire.\nThis is only present if the lease is active.","type":"string"},"gpuGroupId":{"type":"string"},"id":{"type":"string"},"leaseDuration":{"type":"number"},"queuePosition":{"type":"integer"},"userId":{"type":"string"},"vmId":{"description":"VmID is set when the lease is attached to a VM.","type":"string"}},"type":"object"},"body.GpuLeaseUpdate":{"properties":{"vmId":{"description":"VmID is used to specify the VM to attach the lease to.\n\n- If specified, the lease will be attached to the VM.\n\n- If the lease is already attached to a VM, it will be detached from the current VM and attached to the new VM.\n\n- If the lease is not active, specifying a VM will activate the lease.\n\n- If the lease is not assigned, an error will be returned.","type":"string"}},"type":"object"},"body.GpuLeaseUpdated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.GpuStatus":{"properties":{"temp":{"items":{"$ref":"#/components/schemas/body.GpuStatusTemp"},"type":"array","uniqueItems":false}},"type":"object"},"body.GpuStatusTemp":{"properties":{"main":{"type":"number"}},"type":"object"},"body.HarborWebhook":{"properties":{"event_data":{"properties":{"repository":{"properties":{"date_created":{"type":"integer"},"name":{"type":"string"},"namespace":{"type":"string"},"repo_full_name":{"type":"string"},"repo_type":{"type":"string"}},"type":"object"},"resources":{"items":{"properties":{"digest":{"type":"string"},"resource_url":{"type":"string"},"tag":{"type":"string"}},"type":"object"},"type":"array","uniqueItems":false}},"type":"object"},"occur_at":{"type":"integer"},"operator":{"type":"string"},"type":{"type":"string"}},"type":"object"},"body.HostCapacities":{"properties":{"cpuCore":{"$ref":"#/components/schemas/body.CpuCoreCapacities"},"displayName":{"type":"string"},"gpu":{"$ref":"#/components/schemas/body.GpuCapacities"},"name":{"type":"string"},"ram":{"$ref":"#/components/schemas/body.RamCapacities"},"zone":{"description":"Zone is the name of the zone where the host is located.","type":"string"}},"type":"object"},"body.HostRead":{"properties":{"displayName":{"type":"string"},"name":{"type":"string"},"zone":{"description":"Zone is the name of the zone where the host is located.","type":"string"}},"type":"object"},"body.HostStatus":{"properties":{"cpu":{"$ref":"#/components/schemas/body.CpuStatus"},"displayName":{"type":"string"},"gpu":{"$ref":"#/components/schemas/body.GpuStatus"},"name":{"type":"string"},"ram":{"$ref":"#/components/schemas/body.RamStatus"},"zone":{"description":"Zone is the name of the zone where the host is located.","type":"string"}},"type":"object"},"body.HostVerboseRead":{"properties":{"deactivatedUntil":{"type":"string"},"displayName":{"type":"string"},"enabled":{"type":"boolean"},"ip":{"type":"string"},"lastSeenAt":{"type":"string"},"name":{"type":"string"},"port":{"type":"integer"},"registeredAt":{"type":"string"},"schedulable":{"type":"boolean"},"zone":{"description":"Zone is the name of the zone where the host is located.","type":"string"}},"type":"object"},"body.HttpProxyCreate":{"properties":{"customDomain":{"description":"CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.","type":"string"},"name":{"maxLength":30,"minLength":3,"type":"string"}},"required":["name"],"type":"object"},"body.HttpProxyRead":{"properties":{"customDomain":{"$ref":"#/components/schemas/body.CustomDomainRead"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"body.HttpProxyUpdate":{"properties":{"customDomain":{"description":"CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.","type":"string"},"name":{"maxLength":30,"minLength":3,"type":"string"}},"required":["name"],"type":"object"},"body.JobRead":{"properties":{"createdAt":{"type":"string"},"finishedAt":{"type":"string"},"id":{"type":"string"},"lastError":{"type":"string"},"lastRunAt":{"type":"string"},"runAfter":{"type":"string"},"status":{"type":"string"},"type":{"type":"string"},"userId":{"type":"string"}},"type":"object"},"body.JobUpdate":{"properties":{"status":{"enum":["pending","running","failed","terminated","finished","completed"],"type":"string"}},"type":"object"},"body.K8sStats":{"properties":{"clusters":{"items":{"$ref":"#/components/schemas/body.ClusterStats"},"type":"array","uniqueItems":false},"podCount":{"type":"integer"}},"type":"object"},"body.NotificationRead":{"properties":{"completedAt":{"type":"string"},"content":{"additionalProperties":{},"type":"object"},"createdAt":{"type":"string"},"id":{"type":"string"},"readAt":{"type":"string"},"toastedAt":{"type":"string"},"type":{"type":"string"},"userId":{"type":"string"}},"type":"object"},"body.NotificationUpdate":{"properties":{"read":{"type":"boolean"},"toasted":{"type":"boolean"}},"type":"object"},"body.PortCreate":{"properties":{"httpProxy":{"$ref":"#/components/schemas/body.HttpProxyCreate"},"name":{"maxLength":100,"minLength":1,"type":"string"},"port":{"maximum":65535,"minimum":1,"type":"integer"},"protocol":{"enum":["tcp","udp"],"type":"string"}},"required":["name","port","protocol"],"type":"object"},"body.PortRead":{"properties":{"externalPort":{"type":"integer"},"httpProxy":{"$ref":"#/components/schemas/body.HttpProxyRead"},"name":{"type":"string"},"port":{"type":"integer"},"protocol":{"type":"string"}},"type":"object"},"body.PortUpdate":{"properties":{"httpProxy":{"$ref":"#/components/schemas/body.HttpProxyUpdate"},"name":{"maxLength":100,"minLength":1,"type":"string"},"port":{"maximum":65535,"minimum":1,"type":"integer"},"protocol":{"enum":["tcp","udp"],"type":"string"}},"required":["name","port","protocol"],"type":"object"},"body.PublicKey":{"properties":{"key":{"type":"string"},"name":{"maxLength":30,"minLength":1,"type":"string"}},"required":["key","name"],"type":"object"},"body.Quota":{"properties":{"cpuCores":{"type":"number"},"diskSize":{"type":"number"},"gpuLeaseDuration":{"description":"in hours","type":"number"},"ram":{"type":"number"},"snapshots":{"type":"integer"}},"type":"object"},"body.RamCapacities":{"properties":{"total":{"type":"integer"}},"type":"object"},"body.RamStatus":{"properties":{"load":{"$ref":"#/components/schemas/body.RamStatusLoad"}},"type":"object"},"body.RamStatusLoad":{"properties":{"main":{"type":"number"}},"type":"object"},"body.ReplicaStatus":{"properties":{"availableReplicas":{"description":"AvailableReplicas is the number of replicas that are available.","type":"integer"},"desiredReplicas":{"description":"DesiredReplicas is the number of replicas that the deployment should have.","type":"integer"},"readyReplicas":{"description":"ReadyReplicas is the number of replicas that are ready.","type":"integer"},"unavailableReplicas":{"description":"UnavailableReplicas is the number of replicas that are unavailable.","type":"integer"}},"type":"object"},"body.ResourceMigrationCreate":{"properties":{"resourceId":{"description":"ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.","type":"string"},"status":{"description":"Status is the status of the resource migration.\nIt is used by privileged admins to directly accept or reject a migration.\nThe field is ignored by non-admins.\n\nPossible values:\n- accepted\n- pending","type":"string"},"type":{"description":"Type is the type of the resource migration.\n\nPossible values:\n- updateOwner","enum":["updateOwner"],"type":"string"},"updateOwner":{"description":"UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is ignored if the migration type is not updateOwner.","properties":{"ownerId":{"type":"string"}},"required":["ownerId"],"type":"object"}},"required":["resourceId","type"],"type":"object"},"body.ResourceMigrationCreated":{"properties":{"createdAt":{"type":"string"},"deletedAt":{"type":"string"},"id":{"type":"string"},"jobId":{"description":"JobID is the ID of the job that was created for the resource migration.\nIt will only be set if the migration was created with status 'accepted'.","type":"string"},"resourceId":{"description":"ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.","type":"string"},"resourceType":{"description":"ResourceType is the type of the resource that is being migrated.\n\nPossible values:\n- vm\n- deployment","type":"string"},"status":{"description":"Status is the status of the resource migration.\nWhen this field is set to 'accepted', the migration will take place and then automatically be deleted.","type":"string"},"type":{"description":"Type is the type of the resource migration.\n\nPossible values:\n- updateOwner","type":"string"},"updateOwner":{"description":"UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is empty if the migration type is not updateOwner.","properties":{"ownerId":{"type":"string"}},"type":"object"},"userId":{"description":"UserID is the ID of the user who initiated the migration.","type":"string"}},"type":"object"},"body.ResourceMigrationRead":{"properties":{"createdAt":{"type":"string"},"deletedAt":{"type":"string"},"id":{"type":"string"},"resourceId":{"description":"ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.","type":"string"},"resourceType":{"description":"ResourceType is the type of the resource that is being migrated.\n\nPossible values:\n- vm\n- deployment","type":"string"},"status":{"description":"Status is the status of the resource migration.\nWhen this field is set to 'accepted', the migration will take place and then automatically be deleted.","type":"string"},"type":{"description":"Type is the type of the resource migration.\n\nPossible values:\n- updateOwner","type":"string"},"updateOwner":{"description":"UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is empty if the migration type is not updateOwner.","properties":{"ownerId":{"type":"string"}},"type":"object"},"userId":{"description":"UserID is the ID of the user who initiated the migration.","type":"string"}},"type":"object"},"body.ResourceMigrationUpdate":{"properties":{"code":{"description":"Code is a token required when accepting a migration if the acceptor is not an admin.\nIt is sent to the acceptor using the notification API","type":"string"},"status":{"description":"Status is the status of the resource migration.\nIt is used to accept a migration by setting the status to 'accepted'.\nIf the acceptor is not an admin, a Code must be provided.\n\nPossible values:\n- accepted\n- pending","type":"string"}},"required":["status"],"type":"object"},"body.ResourceMigrationUpdated":{"properties":{"createdAt":{"type":"string"},"deletedAt":{"type":"string"},"id":{"type":"string"},"jobId":{"description":"JobID is the ID of the job that was created for the resource migration.\nIt will only be set if the migration was updated with status 'accepted'.","type":"string"},"resourceId":{"description":"ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.","type":"string"},"resourceType":{"description":"ResourceType is the type of the resource that is being migrated.\n\nPossible values:\n- vm\n- deployment","type":"string"},"status":{"description":"Status is the status of the resource migration.\nWhen this field is set to 'accepted', the migration will take place and then automatically be deleted.","type":"string"},"type":{"description":"Type is the type of the resource migration.\n\nPossible values:\n- updateOwner","type":"string"},"updateOwner":{"description":"UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is empty if the migration type is not updateOwner.","properties":{"ownerId":{"type":"string"}},"type":"object"},"userId":{"description":"UserID is the ID of the user who initiated the migration.","type":"string"}},"type":"object"},"body.Role":{"properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"items":{"type":"string"},"type":"array","uniqueItems":false},"quota":{"$ref":"#/components/schemas/body.Quota"}},"type":"object"},"body.SmDeleted":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.SmRead":{"properties":{"createdAt":{"type":"string"},"id":{"type":"string"},"ownerId":{"type":"string"},"url":{"type":"string"},"zone":{"type":"string"}},"type":"object"},"body.SystemCapacities":{"properties":{"clusters":{"description":"Per Cluster","items":{"$ref":"#/components/schemas/body.ClusterCapacities"},"type":"array","uniqueItems":false},"cpuCore":{"$ref":"#/components/schemas/body.CpuCoreCapacities"},"gpu":{"$ref":"#/components/schemas/body.GpuCapacities"},"hosts":{"description":"Per Host","items":{"$ref":"#/components/schemas/body.HostCapacities"},"type":"array","uniqueItems":false},"ram":{"$ref":"#/components/schemas/body.RamCapacities"}},"type":"object"},"body.SystemStats":{"properties":{"k8s":{"$ref":"#/components/schemas/body.K8sStats"}},"type":"object"},"body.SystemStatus":{"properties":{"hosts":{"items":{"$ref":"#/components/schemas/body.HostStatus"},"type":"array","uniqueItems":false}},"type":"object"},"body.TeamCreate":{"properties":{"description":{"maxLength":1000,"type":"string"},"members":{"items":{"$ref":"#/components/schemas/body.TeamMemberCreate"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"name":{"maxLength":100,"minLength":1,"type":"string"},"resources":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false}},"required":["name"],"type":"object"},"body.TeamMember":{"properties":{"addedAt":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"gravatarUrl":{"type":"string"},"id":{"type":"string"},"joinedAt":{"type":"string"},"lastName":{"type":"string"},"memberStatus":{"type":"string"},"teamRole":{"type":"string"},"username":{"type":"string"}},"type":"object"},"body.TeamMemberCreate":{"properties":{"id":{"type":"string"},"teamRole":{"description":"default to MemberRoleAdmin right now","type":"string"}},"required":["id"],"type":"object"},"body.TeamMemberUpdate":{"properties":{"id":{"type":"string"},"teamRole":{"description":"default to MemberRoleAdmin right now","type":"string"}},"required":["id"],"type":"object"},"body.TeamRead":{"properties":{"createdAt":{"type":"string"},"description":{"type":"string"},"id":{"type":"string"},"members":{"items":{"$ref":"#/components/schemas/body.TeamMember"},"type":"array","uniqueItems":false},"name":{"type":"string"},"ownerId":{"type":"string"},"resources":{"items":{"$ref":"#/components/schemas/body.TeamResource"},"type":"array","uniqueItems":false},"updatedAt":{"type":"string"}},"type":"object"},"body.TeamResource":{"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"}},"type":"object"},"body.TeamUpdate":{"properties":{"description":{"maxLength":1000,"type":"string"},"members":{"items":{"$ref":"#/components/schemas/body.TeamMemberUpdate"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"name":{"maxLength":100,"minLength":1,"type":"string"},"resources":{"items":{"type":"string"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false}},"type":"object"},"body.TimestampedSystemCapacities":{"properties":{"capacities":{"$ref":"#/components/schemas/body.SystemCapacities"},"timestamp":{"type":"string"}},"type":"object"},"body.TimestampedSystemStats":{"properties":{"stats":{"$ref":"#/components/schemas/body.SystemStats"},"timestamp":{"type":"string"}},"type":"object"},"body.TimestampedSystemStatus":{"properties":{"status":{"$ref":"#/components/schemas/body.SystemStatus"},"timestamp":{"type":"string"}},"type":"object"},"body.Usage":{"properties":{"cpuCores":{"type":"number"},"diskSize":{"type":"integer"},"ram":{"type":"number"}},"type":"object"},"body.UserData":{"properties":{"key":{"maxLength":255,"minLength":1,"type":"string"},"value":{"maxLength":255,"minLength":1,"type":"string"}},"required":["key","value"],"type":"object"},"body.UserRead":{"properties":{"admin":{"type":"boolean"},"apiKeys":{"items":{"$ref":"#/components/schemas/body.ApiKey"},"type":"array","uniqueItems":false},"email":{"type":"string"},"firstName":{"type":"string"},"gravatarUrl":{"type":"string"},"id":{"type":"string"},"lastName":{"type":"string"},"publicKeys":{"items":{"$ref":"#/components/schemas/body.PublicKey"},"type":"array","uniqueItems":false},"quota":{"$ref":"#/components/schemas/body.Quota"},"role":{"$ref":"#/components/schemas/body.Role"},"storageUrl":{"type":"string"},"usage":{"$ref":"#/components/schemas/body.Usage"},"userData":{"items":{"$ref":"#/components/schemas/body.UserData"},"type":"array","uniqueItems":false},"username":{"type":"string"}},"type":"object"},"body.UserUpdate":{"properties":{"apiKeys":{"description":"ApiKeys specifies the API keys that should remain. If an API key is not in this list, it will be deleted.\nHowever, API keys cannot be created, use /apiKeys endpoint to create new API keys.","items":{"$ref":"#/components/schemas/body.ApiKey"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"publicKeys":{"items":{"$ref":"#/components/schemas/body.PublicKey"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false},"userData":{"items":{"$ref":"#/components/schemas/body.UserData"},"maxItems":100,"minItems":0,"type":"array","uniqueItems":false}},"type":"object"},"body.VmActionCreate":{"properties":{"action":{"enum":["start","stop","restart","repair"],"type":"string"}},"required":["action"],"type":"object"},"body.VmActionCreated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.VmCreate":{"properties":{"cpuCores":{"minimum":1,"type":"integer"},"diskSize":{"minimum":10,"type":"integer"},"name":{"maxLength":30,"minLength":3,"type":"string"},"neverStale":{"type":"boolean"},"ports":{"items":{"$ref":"#/components/schemas/body.PortCreate"},"maxItems":10,"minItems":0,"type":"array","uniqueItems":false},"ram":{"minimum":1,"type":"integer"},"sshPublicKey":{"type":"string"},"zone":{"type":"string"}},"required":["cpuCores","diskSize","name","ram","sshPublicKey"],"type":"object"},"body.VmCreated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.VmDeleted":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.VmGpuLease":{"properties":{"activatedAt":{"description":"ActivatedAt specifies the time when the lease was activated. This is the time the user first attached the GPU\nor 1 day after the lease was created if the user did not attach the GPU.","type":"string"},"assignedAt":{"description":"AssignedAt specifies the time when the lease was assigned to the user.","type":"string"},"createdAt":{"type":"string"},"expiredAt":{"description":"ExpiredAt specifies the time when the lease expired.\nThis is only present if the lease is expired.","type":"string"},"expiresAt":{"description":"ExpiresAt specifies the time when the lease will expire.\nThis is only present if the lease is active.","type":"string"},"gpuGroupId":{"type":"string"},"id":{"type":"string"},"leaseDuration":{"type":"number"}},"type":"object"},"body.VmRead":{"properties":{"accessedAt":{"type":"string"},"createdAt":{"type":"string"},"gpu":{"$ref":"#/components/schemas/body.VmGpuLease"},"host":{"type":"string"},"id":{"type":"string"},"internalName":{"type":"string"},"name":{"type":"string"},"neverStale":{"type":"boolean"},"ownerId":{"type":"string"},"ports":{"items":{"$ref":"#/components/schemas/body.PortRead"},"type":"array","uniqueItems":false},"repairedAt":{"type":"string"},"specs":{"$ref":"#/components/schemas/body.VmSpecs"},"sshConnectionString":{"type":"string"},"sshPublicKey":{"type":"string"},"status":{"type":"string"},"teams":{"items":{"type":"string"},"type":"array","uniqueItems":false},"updatedAt":{"type":"string"},"zone":{"type":"string"}},"type":"object"},"body.VmSnapshotCreated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.VmSnapshotDeleted":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.VmSnapshotRead":{"properties":{"created":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"}},"type":"object"},"body.VmSpecs":{"properties":{"cpuCores":{"type":"integer"},"diskSize":{"type":"integer"},"ram":{"type":"integer"}},"type":"object"},"body.VmUpdate":{"properties":{"cpuCores":{"minimum":1,"type":"integer"},"name":{"maxLength":30,"minLength":3,"type":"string"},"neverStale":{"type":"boolean"},"ports":{"items":{"$ref":"#/components/schemas/body.PortUpdate"},"maxItems":10,"minItems":0,"type":"array","uniqueItems":false},"ram":{"minimum":1,"type":"integer"}},"type":"object"},"body.VmUpdated":{"properties":{"id":{"type":"string"},"jobId":{"type":"string"}},"type":"object"},"body.Volume":{"properties":{"appPath":{"maxLength":255,"minLength":1,"type":"string"},"name":{"maxLength":30,"minLength":3,"type":"string"},"serverPath":{"maxLength":255,"minLength":1,"type":"string"}},"required":["appPath","name","serverPath"],"type":"object"},"body.WorkerStatusRead":{"properties":{"name":{"type":"string"},"reportedAt":{"type":"string"},"status":{"type":"string"}},"type":"object"},"body.ZoneEndpoints":{"properties":{"deployment":{"type":"string"},"storage":{"type":"string"},"vm":{"type":"string"},"vmApp":{"type":"string"}},"type":"object"},"body.ZoneRead":{"properties":{"capabilities":{"items":{"type":"string"},"type":"array","uniqueItems":false},"description":{"type":"string"},"enabled":{"type":"boolean"},"endpoints":{"$ref":"#/components/schemas/body.ZoneEndpoints"},"legacy":{"type":"boolean"},"name":{"type":"string"}},"type":"object"},"sys.Error":{"properties":{"code":{"type":"string"},"msg":{"type":"string"}},"type":"object"},"sys.ErrorResponse":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/sys.Error"},"type":"array","uniqueItems":false}},"type":"object"}},"securitySchemes":{"ApiKeyAuth":{"in":"header","name":"X-Api-Key","type":"apiKey"},"KeycloakOAuth":{"flows":{"authorizationCode":{"authorizationUrl":"https://iam.cloud.cbh.kth.se/realms/cloud/protocol/openid-connect/auth","tokenUrl":"https://iam.cloud.cbh.kth.se/realms/cloud/protocol/openid-connect/token"}},"in":"header","type":"oauth2"}}}, - "info": {"contact":{"name":"Support","url":"https://github.com/kthcloud/go-deploy"},"description":"This is the API explorer for the go-deploy API. You can use it as a reference for the API endpoints.","license":{"name":"MIT License","url":"https://github.com/kthcloud/go-deploy?tab=MIT-1-ov-file#readme"},"termsOfService":"http://swagger.io/terms/","title":"go-deploy API","version":"1.0"}, - "externalDocs": {"description":"","url":""}, - "paths": {"/v2/deployments":{"get":{"description":"List deployments","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by user ID","in":"query","name":"userId","schema":{"type":"string"}},{"description":"Include shared","in":"query","name":"shared","schema":{"type":"boolean"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.DeploymentRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List deployments","tags":["Deployment"]},"post":{"description":"Create deployment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentCreate"}}},"description":"Deployment body","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create deployment","tags":["Deployment"]}},"/v2/deployments/{deploymentId}":{"delete":{"description":"Delete deployment","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete deployment","tags":["Deployment"]},"get":{"description":"Get deployment","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get deployment","tags":["Deployment"]},"post":{"description":"Update deployment","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentUpdate"}}},"description":"Deployment update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentUpdated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update deployment","tags":["Deployment"]}},"/v2/deployments/{deploymentId}/ciConfig":{"get":{"description":"Get CI config","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.CiConfig"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get CI config","tags":["Deployment"]}},"/v2/deployments/{deploymentId}/command":{"post":{"description":"Do command","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DeploymentCommand"}}},"description":"Command body","required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Do command","tags":["Deployment"]}},"/v2/deployments/{deploymentId}/logs":{"get":{"description":"Get logs using Server-Sent Events","parameters":[{"description":"Deployment ID","in":"path","name":"deploymentId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get logs using Server-Sent Events","tags":["Deployment"]}},"/v2/discover":{"get":{"description":"Discover","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.DiscoverRead"}}},"description":"OK"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Discover","tags":["Discover"]}},"/v2/gpuGroups":{"get":{"description":"List GPU groups","parameters":[{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.GpuGroupRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List GPU groups","tags":["GpuGroup"]}},"/v2/gpuGroups/{gpuGroupId}":{"get":{"description":"Get GPU group","parameters":[{"description":"GPU group ID","in":"path","name":"gpuGroupId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuGroupRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get GPU group","tags":["GpuGroup"]}},"/v2/gpuLeases":{"get":{"description":"List GPU leases","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by VM ID","in":"query","name":"vmId","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.GpuLeaseRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List GPU leases","tags":["GpuLease"]},"post":{"description":"Create GPU lease","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseCreate"}}},"description":"GPU lease","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create GPU Lease","tags":["GpuLease"]}},"/v2/gpuLeases/{gpuLeaseId}":{"delete":{"description":"Delete GPU lease","parameters":[{"description":"GPU lease ID","in":"path","name":"gpuLeaseId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseDeleted"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete GPU lease","tags":["GpuLease"]},"get":{"description":"Get GPU lease","parameters":[{"description":"GPU lease ID","in":"path","name":"gpuLeaseId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get GPU lease","tags":["GpuLease"]},"post":{"description":"Update GPU lease","parameters":[{"description":"GPU lease ID","in":"path","name":"gpuLeaseId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseUpdate"}}},"description":"GPU lease","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.GpuLeaseUpdated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update GPU lease","tags":["GpuLease"]}},"/v2/hooks/harbor":{"post":{"description":"Handle Harbor hook","parameters":[{"description":"Basic auth token","in":"header","name":"Authorization","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.HarborWebhook"}}},"description":"Harbor webhook body","required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Handle Harbor hook","tags":["Deployment"]}},"/v2/hosts":{"get":{"description":"List Hosts","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.HostRead"},"type":"array"}}},"description":"OK"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List Hosts","tags":["Host"]}},"/v2/hosts/verbose":{"get":{"description":"List Hosts verbose","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.HostVerboseRead"},"type":"array"}}},"description":"OK"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List Hosts verbose","tags":["Host"]}},"/v2/jobs":{"get":{"description":"List jobs","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by user ID","in":"query","name":"userId","schema":{"type":"string"}},{"description":"Filter by type","in":"query","name":"type","schema":{"type":"string"}},{"description":"Filter by status","in":"query","name":"status","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.JobRead"},"type":"array"}}},"description":"OK"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List jobs","tags":["Job"]}},"/v2/jobs/{jobId}":{"get":{"description":"GetJob job by id","parameters":[{"description":"Job ID","in":"path","name":"jobId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.JobRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"GetJob job by id","tags":["Job"]},"post":{"description":"Update job. Only allowed for admins.","parameters":[{"description":"Job ID","in":"path","name":"jobId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.JobUpdate"}}},"description":"Job update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.JobRead"}}},"description":"OK"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update job","tags":["Job"]}},"/v2/metrics":{"get":{"description":"Get metrics","responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Get metrics","tags":["Metrics"]}},"/v2/notifications":{"get":{"description":"List notifications","parameters":[{"description":"List all notifications","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by user ID","in":"query","name":"userId","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.NotificationRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List notifications","tags":["Notification"]}},"/v2/notifications/{notificationId}":{"delete":{"description":"Delete notification","parameters":[{"description":"Notification ID","in":"path","name":"notificationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete notification","tags":["Notification"]},"get":{"description":"Get notification","parameters":[{"description":"Notification ID","in":"path","name":"notificationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.NotificationRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get notification","tags":["Notification"]},"post":{"description":"Update notification","parameters":[{"description":"Notification ID","in":"path","name":"notificationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.NotificationUpdate"}}},"description":"Notification update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update notification","tags":["Notification"]}},"/v2/register":{"get":{"description":"Register resource","responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Register resource","tags":["Register"]}},"/v2/resourceMigrations":{"get":{"description":"List resource migrations","parameters":[{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.ResourceMigrationRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List resource migrations","tags":["ResourceMigration"]},"post":{"description":"Create resource migration","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ResourceMigrationCreate"}}},"description":"Resource Migration Create","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ResourceMigrationCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create resource migration","tags":["ResourceMigration"]}},"/v2/resourceMigrations/{resourceMigrationId}":{"delete":{"description":"Delete resource migration","parameters":[{"description":"Resource Migration ID","in":"path","name":"resourceMigrationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete resource migration","tags":["ResourceMigration"]},"get":{"description":"Get resource migration","parameters":[{"description":"Resource Migration ID","in":"path","name":"resourceMigrationId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ResourceMigrationRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get resource migration","tags":["ResourceMigration"]},"post":{"description":"Update resource migration","parameters":[{"description":"Resource Migration ID","in":"path","name":"resourceMigrationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ResourceMigrationUpdate"}}},"description":"Resource Migration Update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ResourceMigrationUpdated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update resource migration","tags":["ResourceMigration"]}},"/v2/snapshots":{"get":{"description":"List snapshots","parameters":[{"description":"Filter by VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.VmSnapshotRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"423":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Locked"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List snapshots","tags":["Snapshot"]},"post":{"description":"Create snapshot","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmSnapshotCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create snapshot","tags":["Snapshot"]}},"/v2/storageManagers":{"get":{"description":"Get storage manager list","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.SmRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get storage manager list","tags":["StorageManager"]}},"/v2/storageManagers/{storageManagerId}":{"delete":{"description":"Delete storage manager","parameters":[{"description":"Storage manager ID","in":"path","name":"storageManagerId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.SmDeleted"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete storage manager","tags":["StorageManager"]},"get":{"description":"Get storage manager","parameters":[{"description":"Storage manager ID","in":"path","name":"storageManagerId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.SmDeleted"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get storage manager","tags":["StorageManager"]}},"/v2/systemCapacities":{"get":{"description":"List system capacities","parameters":[{"description":"n","in":"query","name":"n","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.TimestampedSystemCapacities"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List system capacities","tags":["System"]}},"/v2/systemStats":{"get":{"description":"List system stats","parameters":[{"description":"n","in":"query","name":"n","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.TimestampedSystemStats"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List system stats","tags":["System"]}},"/v2/systemStatus":{"get":{"description":"List system stats","parameters":[{"description":"n","in":"query","name":"n","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.TimestampedSystemStatus"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List system stats","tags":["System"]}},"/v2/teams":{"get":{"description":"List teams","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by user ID","in":"query","name":"userId","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.TeamRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List teams","tags":["Team"]},"post":{"description":"Create team","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.TeamCreate"}}},"description":"Team","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.TeamRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create team","tags":["Team"]}},"/v2/teams/{teamId}":{"delete":{"description":"Delete team","parameters":[{"description":"Team ID","in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete team","tags":["Team"]},"get":{"description":"Get team","parameters":[{"description":"Team ID","in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.TeamRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get team","tags":["Team"]},"post":{"description":"Update team","parameters":[{"description":"Team ID","in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.TeamUpdate"}}},"description":"Team","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.TeamRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.BindingError"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update team","tags":["Team"]}},"/v2/users":{"get":{"description":"List users","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Discovery mode","in":"query","name":"discover","schema":{"type":"boolean"}},{"description":"Search query","in":"query","name":"search","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.UserRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List users","tags":["User"]}},"/v2/users/{userId}":{"get":{"description":"Get user","parameters":[{"description":"User ID","in":"path","name":"userId","required":true,"schema":{"type":"string"}},{"description":"Discovery mode","in":"query","name":"discover","schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.UserRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get user","tags":["User"]},"post":{"description":"Update user","parameters":[{"description":"User ID","in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.UserUpdate"}}},"description":"User update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.UserRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update user","tags":["User"]}},"/v2/users/{userId}/apiKeys":{"post":{"description":"Create API key","parameters":[{"description":"User ID","in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ApiKeyCreate"}}},"description":"API key create body","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.ApiKeyCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create API key","tags":["User"]}},"/v2/vmActions":{"post":{"description":"Creates a new action","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmActionCreate"}}},"description":"actions body","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmActionCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Creates a new action","tags":["VmAction"]}},"/v2/vms":{"get":{"description":"List VMs","parameters":[{"description":"List all","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter by user ID","in":"query","name":"userId","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Number of items per page","in":"query","name":"pageSize","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.VmRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List VMs","tags":["VM"]},"post":{"description":"Create VM","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmCreate"}}},"description":"VM body","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmCreated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Create VM","tags":["VM"]}},"/v2/vms/{vmId}":{"delete":{"description":"Delete VM","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmDeleted"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete VM","tags":["VM"]},"get":{"description":"Get VM","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get VM","tags":["VM"]},"post":{"description":"Update VM","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmUpdate"}}},"description":"VM update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmUpdated"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Update VM","tags":["VM"]}},"/v2/vms/{vmId}/snapshot/{snapshotId}":{"delete":{"description":"Delete snapshot","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}},{"description":"Snapshot ID","in":"path","name":"snapshotId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmSnapshotDeleted"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Delete snapshot","tags":["Snapshot"]},"post":{"description":"Get snapshot","parameters":[{"description":"VM ID","in":"path","name":"vmId","required":true,"schema":{"type":"string"}},{"description":"Snapshot ID","in":"path","name":"snapshotId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/body.VmSnapshotRead"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"Get snapshot","tags":["Snapshot"]}},"/v2/workerStatus":{"get":{"description":"List of worker status","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.WorkerStatusRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List worker status","tags":["Status"]}},"/v2/zones":{"get":{"description":"List zones","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/body.ZoneRead"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sys.ErrorResponse"}}},"description":"Internal Server Error"}},"security":[{"ApiKeyAuth":[]},{"KeycloakOAuth":[]}],"summary":"List zones","tags":["Zone"]}}}, + "components": { + "schemas": { + "body.AllocatedGpu": { + "properties": { + "adminAccess": { + "type": "boolean" + }, + "device": { + "type": "string" + }, + "pool": { + "type": "string" + }, + "shareID": { + "type": "string" + } + }, + "type": "object" + }, + "body.ApiKey": { + "properties": { + "createdAt": { + "type": "string" + }, + "expiresAt": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "body.ApiKeyCreate": { + "properties": { + "expiresAt": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "expiresAt", + "name" + ], + "type": "object" + }, + "body.ApiKeyCreated": { + "properties": { + "createdAt": { + "type": "string" + }, + "expiresAt": { + "type": "string" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "body.BindingError": { + "properties": { + "validationErrors": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "object" + } + }, + "type": "object" + }, + "body.CiConfig": { + "properties": { + "config": { + "type": "string" + } + }, + "type": "object" + }, + "body.ClusterCapacities": { + "properties": { + "cluster": { + "type": "string" + }, + "cpuCore": { + "$ref": "#/components/schemas/body.CpuCoreCapacities" + }, + "gpu": { + "$ref": "#/components/schemas/body.GpuCapacities" + }, + "ram": { + "$ref": "#/components/schemas/body.RamCapacities" + } + }, + "type": "object" + }, + "body.ClusterStats": { + "properties": { + "cluster": { + "type": "string" + }, + "podCount": { + "type": "integer" + } + }, + "type": "object" + }, + "body.CpuCoreCapacities": { + "description": "Total", + "properties": { + "total": { + "type": "integer" + } + }, + "type": "object" + }, + "body.CpuStatus": { + "properties": { + "load": { + "$ref": "#/components/schemas/body.CpuStatusLoad" + }, + "temp": { + "$ref": "#/components/schemas/body.CpuStatusTemp" + } + }, + "type": "object" + }, + "body.CpuStatusLoad": { + "properties": { + "cores": { + "items": { + "type": "integer" + }, + "type": "array", + "uniqueItems": false + }, + "main": { + "type": "number" + }, + "max": { + "type": "number" + } + }, + "type": "object" + }, + "body.CpuStatusTemp": { + "properties": { + "cores": { + "items": { + "type": "integer" + }, + "type": "array", + "uniqueItems": false + }, + "main": { + "type": "number" + }, + "max": { + "type": "number" + } + }, + "type": "object" + }, + "body.CustomDomainRead": { + "properties": { + "domain": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "status": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "body.DeploymentCommand": { + "properties": { + "command": { + "enum": [ + "restart" + ], + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + }, + "body.DeploymentCreate": { + "properties": { + "args": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "cpuCores": { + "type": "number" + }, + "customDomain": { + "description": "CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.", + "type": "string" + }, + "envs": { + "items": { + "$ref": "#/components/schemas/body.Env" + }, + "maxItems": 1000, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "gpus": { + "items": { + "$ref": "#/components/schemas/body.DeploymentGPU" + }, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "healthCheckPath": { + "maxLength": 1000, + "minLength": 0, + "type": "string" + }, + "image": { + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + "initCommands": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "neverStale": { + "description": "Boolean to make deployment never get disabled, despite being stale", + "type": "boolean" + }, + "private": { + "description": "Deprecated: Use Visibility instead.", + "type": "boolean" + }, + "ram": { + "type": "number" + }, + "replicas": { + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + "visibility": { + "enum": [ + "public", + "private", + "auth" + ], + "type": "string" + }, + "volumes": { + "items": { + "$ref": "#/components/schemas/body.Volume" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "zone": { + "description": "Zone is the zone that the deployment will be created in.\nIf the zone is not set, the deployment will be created in the default zone.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.DeploymentCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.DeploymentGPU": { + "properties": { + "claimName": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "claimName", + "name" + ], + "type": "object" + }, + "body.DeploymentRead": { + "properties": { + "accessedAt": { + "type": "string" + }, + "args": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "createdAt": { + "type": "string" + }, + "customDomain": { + "$ref": "#/components/schemas/body.CustomDomainRead" + }, + "envs": { + "items": { + "$ref": "#/components/schemas/body.Env" + }, + "type": "array", + "uniqueItems": false + }, + "error": { + "type": "string" + }, + "healthCheckPath": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image": { + "type": "string" + }, + "initCommands": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "integrations": { + "description": "Integrations are currently not used, but could be used if we wanted to add a list of integrations to the deployment\n\nFor example GitHub", + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "internalPort": { + "type": "integer" + }, + "internalPorts": { + "items": { + "type": "integer" + }, + "type": "array", + "uniqueItems": false + }, + "name": { + "type": "string" + }, + "neverStale": { + "type": "boolean" + }, + "ownerId": { + "type": "string" + }, + "pingResult": { + "type": "integer" + }, + "private": { + "description": "Deprecated: Use Visibility instead.", + "type": "boolean" + }, + "repairedAt": { + "type": "string" + }, + "replicaStatus": { + "$ref": "#/components/schemas/body.ReplicaStatus" + }, + "restartedAt": { + "type": "string" + }, + "specs": { + "$ref": "#/components/schemas/body.DeploymentSpecs" + }, + "status": { + "type": "string" + }, + "storageUrl": { + "type": "string" + }, + "teams": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + }, + "visibility": { + "type": "string" + }, + "volumes": { + "items": { + "$ref": "#/components/schemas/body.Volume" + }, + "type": "array", + "uniqueItems": false + }, + "zone": { + "type": "string" + } + }, + "type": "object" + }, + "body.DeploymentSpecs": { + "properties": { + "cpuCores": { + "type": "number" + }, + "gpus": { + "items": { + "$ref": "#/components/schemas/body.DeploymentGPU" + }, + "type": "array", + "uniqueItems": false + }, + "ram": { + "type": "number" + }, + "replicas": { + "type": "integer" + } + }, + "type": "object" + }, + "body.DeploymentUpdate": { + "properties": { + "args": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "cpuCores": { + "type": "number" + }, + "customDomain": { + "description": "CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.", + "type": "string" + }, + "envs": { + "items": { + "$ref": "#/components/schemas/body.Env" + }, + "maxItems": 1000, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "gpus": { + "items": { + "$ref": "#/components/schemas/body.DeploymentGPU" + }, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "healthCheckPath": { + "maxLength": 1000, + "minLength": 0, + "type": "string" + }, + "image": { + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + "initCommands": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "neverStale": { + "type": "boolean" + }, + "private": { + "description": "Deprecated: Use Visibility instead.", + "type": "boolean" + }, + "ram": { + "type": "number" + }, + "replicas": { + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + "visibility": { + "enum": [ + "public", + "private", + "auth" + ], + "type": "string" + }, + "volumes": { + "items": { + "$ref": "#/components/schemas/body.Volume" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.DeploymentUpdated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.DiscoverRead": { + "properties": { + "roles": { + "items": { + "$ref": "#/components/schemas/body.Role" + }, + "type": "array", + "uniqueItems": false + }, + "version": { + "type": "string" + } + }, + "type": "object" + }, + "body.Env": { + "properties": { + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "value": { + "maxLength": 10000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "body.GpuCapacities": { + "properties": { + "total": { + "type": "integer" + } + }, + "type": "object" + }, + "body.GpuClaimConsumer": { + "properties": { + "apiGroup": { + "type": "string" + }, + "name": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "uid": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuClaimCreate": { + "properties": { + "allowedRoles": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "requested": { + "description": "Requested contains all requested GPU configurations by key (request.Name).", + "items": { + "$ref": "#/components/schemas/body.RequestedGpuCreate" + }, + "minItems": 1, + "type": "array", + "uniqueItems": false + }, + "zone": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.GpuClaimCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuClaimRead": { + "properties": { + "allocated": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/body.AllocatedGpu" + }, + "type": "array" + }, + "description": "Allocated contains the GPUs that have been successfully bound/allocated.", + "type": "object" + }, + "allowedRoles": { + "description": "Roles allowed to use this GpuClaim, empty means all", + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "consumers": { + "description": "Consumers are the workloads currently using this claim.", + "items": { + "$ref": "#/components/schemas/body.GpuClaimConsumer" + }, + "type": "array", + "uniqueItems": false + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastError": { + "description": "LastError holds the last reconciliation or provisioning error message.", + "type": "string" + }, + "name": { + "type": "string" + }, + "requested": { + "additionalProperties": { + "$ref": "#/components/schemas/body.RequestedGpu" + }, + "description": "Requested contains all requested GPU configurations by key (request.Name).", + "type": "object" + }, + "status": { + "$ref": "#/components/schemas/body.GpuClaimStatus" + }, + "updatedAt": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuClaimStatus": { + "description": "Status reflects the reconciliation and/or lifecycle state.", + "properties": { + "lastSynced": { + "type": "string" + }, + "message": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuDeviceConfigurationWrapper": { + "type": "object" + }, + "body.GpuGroupRead": { + "properties": { + "available": { + "type": "integer" + }, + "displayName": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "total": { + "type": "integer" + }, + "vendor": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuLeaseCreate": { + "properties": { + "gpuGroupId": { + "description": "GpuGroupID is used to specify the GPU to lease.\nAs such, the lease does not specify which specific GPU to lease, but rather the type of GPU to lease.", + "type": "string" + }, + "leaseForever": { + "description": "LeaseForever is used to specify whether the lease should be created forever.", + "type": "boolean" + } + }, + "required": [ + "gpuGroupId" + ], + "type": "object" + }, + "body.GpuLeaseCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuLeaseDeleted": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuLeaseRead": { + "properties": { + "activatedAt": { + "description": "ActivatedAt specifies the time when the lease was activated. This is the time the user first attached the GPU\nor 1 day after the lease was created if the user did not attach the GPU.", + "type": "string" + }, + "active": { + "type": "boolean" + }, + "assignedAt": { + "description": "AssignedAt specifies the time when the lease was assigned to the user.", + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "expiredAt": { + "type": "string" + }, + "expiresAt": { + "description": "ExpiresAt specifies the time when the lease will expire.\nThis is only present if the lease is active.", + "type": "string" + }, + "gpuGroupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "leaseDuration": { + "type": "number" + }, + "queuePosition": { + "type": "integer" + }, + "userId": { + "type": "string" + }, + "vmId": { + "description": "VmID is set when the lease is attached to a VM.", + "type": "string" + } + }, + "type": "object" + }, + "body.GpuLeaseUpdate": { + "properties": { + "vmId": { + "description": "VmID is used to specify the VM to attach the lease to.\n\n- If specified, the lease will be attached to the VM.\n\n- If the lease is already attached to a VM, it will be detached from the current VM and attached to the new VM.\n\n- If the lease is not active, specifying a VM will activate the lease.\n\n- If the lease is not assigned, an error will be returned.", + "type": "string" + } + }, + "type": "object" + }, + "body.GpuLeaseUpdated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.GpuStatus": { + "properties": { + "temp": { + "items": { + "$ref": "#/components/schemas/body.GpuStatusTemp" + }, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "body.GpuStatusTemp": { + "properties": { + "main": { + "type": "number" + } + }, + "type": "object" + }, + "body.HarborWebhook": { + "properties": { + "event_data": { + "properties": { + "repository": { + "properties": { + "date_created": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "repo_full_name": { + "type": "string" + }, + "repo_type": { + "type": "string" + } + }, + "type": "object" + }, + "resources": { + "items": { + "properties": { + "digest": { + "type": "string" + }, + "resource_url": { + "type": "string" + }, + "tag": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "occur_at": { + "type": "integer" + }, + "operator": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "body.HostCapacities": { + "properties": { + "cpuCore": { + "$ref": "#/components/schemas/body.CpuCoreCapacities" + }, + "gpu": { + "$ref": "#/components/schemas/body.GpuCapacities" + }, + "ram": { + "$ref": "#/components/schemas/body.RamCapacities" + } + }, + "type": "object" + }, + "body.HostRead": { + "type": "object" + }, + "body.HostStatus": { + "properties": { + "cpu": { + "$ref": "#/components/schemas/body.CpuStatus" + }, + "gpu": { + "$ref": "#/components/schemas/body.GpuStatus" + }, + "ram": { + "$ref": "#/components/schemas/body.RamStatus" + } + }, + "type": "object" + }, + "body.HostVerboseRead": { + "properties": { + "deactivatedUntil": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "ip": { + "type": "string" + }, + "lastSeenAt": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "registeredAt": { + "type": "string" + }, + "schedulable": { + "type": "boolean" + } + }, + "type": "object" + }, + "body.HttpProxyCreate": { + "properties": { + "customDomain": { + "description": "CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.", + "type": "string" + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.HttpProxyRead": { + "properties": { + "customDomain": { + "$ref": "#/components/schemas/body.CustomDomainRead" + }, + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "body.HttpProxyUpdate": { + "properties": { + "customDomain": { + "description": "CustomDomain is the domain that the deployment will be available on.\nThe max length is set to 243 to allow for a subdomain when confirming the domain.", + "type": "string" + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.JobRead": { + "properties": { + "createdAt": { + "type": "string" + }, + "finishedAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastError": { + "type": "string" + }, + "lastRunAt": { + "type": "string" + }, + "runAfter": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object" + }, + "body.JobUpdate": { + "properties": { + "status": { + "enum": [ + "pending", + "running", + "failed", + "terminated", + "finished", + "completed" + ], + "type": "string" + } + }, + "type": "object" + }, + "body.K8sStats": { + "properties": { + "clusters": { + "items": { + "$ref": "#/components/schemas/body.ClusterStats" + }, + "type": "array", + "uniqueItems": false + }, + "podCount": { + "type": "integer" + } + }, + "type": "object" + }, + "body.NotificationRead": { + "properties": { + "completedAt": { + "type": "string" + }, + "content": { + "additionalProperties": {}, + "type": "object" + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "readAt": { + "type": "string" + }, + "toastedAt": { + "type": "string" + }, + "type": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object" + }, + "body.NotificationUpdate": { + "properties": { + "read": { + "type": "boolean" + }, + "toasted": { + "type": "boolean" + } + }, + "type": "object" + }, + "body.PortCreate": { + "properties": { + "httpProxy": { + "$ref": "#/components/schemas/body.HttpProxyCreate" + }, + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "port": { + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "protocol": { + "enum": [ + "tcp", + "udp" + ], + "type": "string" + } + }, + "required": [ + "name", + "port", + "protocol" + ], + "type": "object" + }, + "body.PortRead": { + "properties": { + "externalPort": { + "type": "integer" + }, + "httpProxy": { + "$ref": "#/components/schemas/body.HttpProxyRead" + }, + "name": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "protocol": { + "type": "string" + } + }, + "type": "object" + }, + "body.PortUpdate": { + "properties": { + "httpProxy": { + "$ref": "#/components/schemas/body.HttpProxyUpdate" + }, + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "port": { + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "protocol": { + "enum": [ + "tcp", + "udp" + ], + "type": "string" + } + }, + "required": [ + "name", + "port", + "protocol" + ], + "type": "object" + }, + "body.PublicKey": { + "properties": { + "key": { + "type": "string" + }, + "name": { + "maxLength": 30, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "key", + "name" + ], + "type": "object" + }, + "body.Quota": { + "properties": { + "cpuCores": { + "type": "number" + }, + "diskSize": { + "type": "number" + }, + "gpuLeaseDuration": { + "description": "in hours", + "type": "number" + }, + "gpus": { + "type": "integer" + }, + "ram": { + "type": "number" + }, + "snapshots": { + "type": "integer" + } + }, + "type": "object" + }, + "body.RamCapacities": { + "properties": { + "total": { + "type": "integer" + } + }, + "type": "object" + }, + "body.RamStatus": { + "properties": { + "load": { + "$ref": "#/components/schemas/body.RamStatusLoad" + } + }, + "type": "object" + }, + "body.RamStatusLoad": { + "properties": { + "main": { + "type": "number" + } + }, + "type": "object" + }, + "body.ReplicaStatus": { + "properties": { + "availableReplicas": { + "description": "AvailableReplicas is the number of replicas that are available.", + "type": "integer" + }, + "desiredReplicas": { + "description": "DesiredReplicas is the number of replicas that the deployment should have.", + "type": "integer" + }, + "readyReplicas": { + "description": "ReadyReplicas is the number of replicas that are ready.", + "type": "integer" + }, + "unavailableReplicas": { + "description": "UnavailableReplicas is the number of replicas that are unavailable.", + "type": "integer" + } + }, + "type": "object" + }, + "body.RequestedGpu": { + "properties": { + "allocationMode": { + "enum": [ + "All", + "ExactCount" + ], + "type": "string" + }, + "capacity": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "config": { + "$ref": "#/components/schemas/body.GpuDeviceConfigurationWrapper" + }, + "count": { + "type": "integer" + }, + "deviceClassName": { + "type": "string" + }, + "selectors": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + } + }, + "required": [ + "allocationMode", + "deviceClassName" + ], + "type": "object" + }, + "body.RequestedGpuCreate": { + "properties": { + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.ResourceMigrationCreate": { + "properties": { + "resourceId": { + "description": "ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.", + "type": "string" + }, + "status": { + "description": "Status is the status of the resource migration.\nIt is used by privileged admins to directly accept or reject a migration.\nThe field is ignored by non-admins.\n\nPossible values:\n- accepted\n- pending", + "type": "string" + }, + "type": { + "description": "Type is the type of the resource migration.\n\nPossible values:\n- updateOwner", + "enum": [ + "updateOwner" + ], + "type": "string" + }, + "updateOwner": { + "description": "UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is ignored if the migration type is not updateOwner.", + "properties": { + "ownerId": { + "type": "string" + } + }, + "required": [ + "ownerId" + ], + "type": "object" + } + }, + "required": [ + "resourceId", + "type" + ], + "type": "object" + }, + "body.ResourceMigrationCreated": { + "properties": { + "jobId": { + "description": "JobID is the ID of the job that was created for the resource migration.\nIt will only be set if the migration was created with status 'accepted'.", + "type": "string" + } + }, + "type": "object" + }, + "body.ResourceMigrationRead": { + "properties": { + "createdAt": { + "type": "string" + }, + "deletedAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "resourceId": { + "description": "ResourceID is the ID of the resource that is being migrated.\nThis can be a VM ID, deployment ID, etc. depending on the type of the migration.", + "type": "string" + }, + "resourceType": { + "description": "ResourceType is the type of the resource that is being migrated.\n\nPossible values:\n- vm\n- deployment", + "type": "string" + }, + "status": { + "description": "Status is the status of the resource migration.\nWhen this field is set to 'accepted', the migration will take place and then automatically be deleted.", + "type": "string" + }, + "type": { + "description": "Type is the type of the resource migration.\n\nPossible values:\n- updateOwner", + "type": "string" + }, + "updateOwner": { + "description": "UpdateOwner is the set of parameters that are required for the updateOwner migration type.\nIt is empty if the migration type is not updateOwner.", + "properties": { + "ownerId": { + "type": "string" + } + }, + "type": "object" + }, + "userId": { + "description": "UserID is the ID of the user who initiated the migration.", + "type": "string" + } + }, + "type": "object" + }, + "body.ResourceMigrationUpdate": { + "properties": { + "code": { + "description": "Code is a token required when accepting a migration if the acceptor is not an admin.\nIt is sent to the acceptor using the notification API", + "type": "string" + }, + "status": { + "description": "Status is the status of the resource migration.\nIt is used to accept a migration by setting the status to 'accepted'.\nIf the acceptor is not an admin, a Code must be provided.\n\nPossible values:\n- accepted\n- pending", + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "body.ResourceMigrationUpdated": { + "properties": { + "jobId": { + "description": "JobID is the ID of the job that was created for the resource migration.\nIt will only be set if the migration was updated with status 'accepted'.", + "type": "string" + } + }, + "type": "object" + }, + "body.Role": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "permissions": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "quota": { + "$ref": "#/components/schemas/body.Quota" + } + }, + "type": "object" + }, + "body.SmDeleted": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.SmRead": { + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "url": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "type": "object" + }, + "body.SystemCapacities": { + "properties": { + "clusters": { + "description": "Per Cluster", + "items": { + "$ref": "#/components/schemas/body.ClusterCapacities" + }, + "type": "array", + "uniqueItems": false + }, + "cpuCore": { + "$ref": "#/components/schemas/body.CpuCoreCapacities" + }, + "gpu": { + "$ref": "#/components/schemas/body.GpuCapacities" + }, + "hosts": { + "description": "Per Host", + "items": { + "$ref": "#/components/schemas/body.HostCapacities" + }, + "type": "array", + "uniqueItems": false + }, + "ram": { + "$ref": "#/components/schemas/body.RamCapacities" + } + }, + "type": "object" + }, + "body.SystemStats": { + "properties": { + "k8s": { + "$ref": "#/components/schemas/body.K8sStats" + } + }, + "type": "object" + }, + "body.SystemStatus": { + "properties": { + "hosts": { + "items": { + "$ref": "#/components/schemas/body.HostStatus" + }, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "body.TeamCreate": { + "properties": { + "description": { + "maxLength": 1000, + "type": "string" + }, + "members": { + "items": { + "$ref": "#/components/schemas/body.TeamMemberCreate" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "resources": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "body.TeamMember": { + "properties": { + "addedAt": { + "type": "string" + }, + "joinedAt": { + "type": "string" + }, + "memberStatus": { + "type": "string" + }, + "teamRole": { + "type": "string" + } + }, + "type": "object" + }, + "body.TeamMemberCreate": { + "properties": { + "id": { + "type": "string" + }, + "teamRole": { + "description": "default to MemberRoleAdmin right now", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "body.TeamMemberUpdate": { + "properties": { + "id": { + "type": "string" + }, + "teamRole": { + "description": "default to MemberRoleAdmin right now", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "body.TeamRead": { + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "members": { + "items": { + "$ref": "#/components/schemas/body.TeamMember" + }, + "type": "array", + "uniqueItems": false + }, + "name": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "resources": { + "items": { + "$ref": "#/components/schemas/body.TeamResource" + }, + "type": "array", + "uniqueItems": false + }, + "updatedAt": { + "type": "string" + } + }, + "type": "object" + }, + "body.TeamResource": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "body.TeamUpdate": { + "properties": { + "description": { + "maxLength": 1000, + "type": "string" + }, + "members": { + "items": { + "$ref": "#/components/schemas/body.TeamMemberUpdate" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "name": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "resources": { + "items": { + "type": "string" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "body.TimestampedSystemCapacities": { + "properties": { + "capacities": { + "$ref": "#/components/schemas/body.SystemCapacities" + }, + "timestamp": { + "type": "string" + } + }, + "type": "object" + }, + "body.TimestampedSystemStats": { + "properties": { + "stats": { + "$ref": "#/components/schemas/body.SystemStats" + }, + "timestamp": { + "type": "string" + } + }, + "type": "object" + }, + "body.TimestampedSystemStatus": { + "properties": { + "status": { + "$ref": "#/components/schemas/body.SystemStatus" + }, + "timestamp": { + "type": "string" + } + }, + "type": "object" + }, + "body.Usage": { + "properties": { + "cpuCores": { + "type": "number" + }, + "diskSize": { + "type": "integer" + }, + "gpus": { + "type": "integer" + }, + "ram": { + "type": "number" + } + }, + "type": "object" + }, + "body.UserData": { + "properties": { + "key": { + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "value": { + "maxLength": 255, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, + "body.UserRead": { + "properties": { + "admin": { + "type": "boolean" + }, + "apiKeys": { + "items": { + "$ref": "#/components/schemas/body.ApiKey" + }, + "type": "array", + "uniqueItems": false + }, + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "gravatarUrl": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "publicKeys": { + "items": { + "$ref": "#/components/schemas/body.PublicKey" + }, + "type": "array", + "uniqueItems": false + }, + "quota": { + "$ref": "#/components/schemas/body.Quota" + }, + "role": { + "$ref": "#/components/schemas/body.Role" + }, + "storageUrl": { + "type": "string" + }, + "usage": { + "$ref": "#/components/schemas/body.Usage" + }, + "userData": { + "items": { + "$ref": "#/components/schemas/body.UserData" + }, + "type": "array", + "uniqueItems": false + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "body.UserUpdate": { + "properties": { + "apiKeys": { + "description": "ApiKeys specifies the API keys that should remain. If an API key is not in this list, it will be deleted.\nHowever, API keys cannot be created, use /apiKeys endpoint to create new API keys.", + "items": { + "$ref": "#/components/schemas/body.ApiKey" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "publicKeys": { + "items": { + "$ref": "#/components/schemas/body.PublicKey" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "userData": { + "items": { + "$ref": "#/components/schemas/body.UserData" + }, + "maxItems": 100, + "minItems": 0, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + }, + "body.VmActionCreate": { + "properties": { + "action": { + "enum": [ + "start", + "stop", + "restart", + "repair" + ], + "type": "string" + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "body.VmActionCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmCreate": { + "properties": { + "cpuCores": { + "minimum": 1, + "type": "integer" + }, + "diskSize": { + "minimum": 10, + "type": "integer" + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "neverStale": { + "type": "boolean" + }, + "ports": { + "items": { + "$ref": "#/components/schemas/body.PortCreate" + }, + "maxItems": 10, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "ram": { + "minimum": 1, + "type": "integer" + }, + "sshPublicKey": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "required": [ + "cpuCores", + "diskSize", + "name", + "ram", + "sshPublicKey" + ], + "type": "object" + }, + "body.VmCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmDeleted": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmGpuLease": { + "properties": { + "activatedAt": { + "description": "ActivatedAt specifies the time when the lease was activated. This is the time the user first attached the GPU\nor 1 day after the lease was created if the user did not attach the GPU.", + "type": "string" + }, + "assignedAt": { + "description": "AssignedAt specifies the time when the lease was assigned to the user.", + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "expiredAt": { + "description": "ExpiredAt specifies the time when the lease expired.\nThis is only present if the lease is expired.", + "type": "string" + }, + "expiresAt": { + "description": "ExpiresAt specifies the time when the lease will expire.\nThis is only present if the lease is active.", + "type": "string" + }, + "gpuGroupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "leaseDuration": { + "type": "number" + } + }, + "type": "object" + }, + "body.VmRead": { + "properties": { + "accessedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "gpu": { + "$ref": "#/components/schemas/body.VmGpuLease" + }, + "host": { + "type": "string" + }, + "id": { + "type": "string" + }, + "internalName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "neverStale": { + "type": "boolean" + }, + "ownerId": { + "type": "string" + }, + "ports": { + "items": { + "$ref": "#/components/schemas/body.PortRead" + }, + "type": "array", + "uniqueItems": false + }, + "repairedAt": { + "type": "string" + }, + "specs": { + "$ref": "#/components/schemas/body.VmSpecs" + }, + "sshConnectionString": { + "type": "string" + }, + "sshPublicKey": { + "type": "string" + }, + "status": { + "type": "string" + }, + "teams": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "updatedAt": { + "type": "string" + }, + "zone": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmSnapshotCreated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmSnapshotDeleted": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmSnapshotRead": { + "properties": { + "created": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "body.VmSpecs": { + "properties": { + "cpuCores": { + "type": "integer" + }, + "diskSize": { + "type": "integer" + }, + "ram": { + "type": "integer" + } + }, + "type": "object" + }, + "body.VmUpdate": { + "properties": { + "cpuCores": { + "minimum": 1, + "type": "integer" + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "neverStale": { + "type": "boolean" + }, + "ports": { + "items": { + "$ref": "#/components/schemas/body.PortUpdate" + }, + "maxItems": 10, + "minItems": 0, + "type": "array", + "uniqueItems": false + }, + "ram": { + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "body.VmUpdated": { + "properties": { + "id": { + "type": "string" + }, + "jobId": { + "type": "string" + } + }, + "type": "object" + }, + "body.Volume": { + "properties": { + "appPath": { + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 30, + "minLength": 3, + "type": "string" + }, + "serverPath": { + "maxLength": 255, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "appPath", + "name", + "serverPath" + ], + "type": "object" + }, + "body.WorkerStatusRead": { + "properties": { + "name": { + "type": "string" + }, + "reportedAt": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "body.ZoneEndpoints": { + "properties": { + "deployment": { + "type": "string" + }, + "storage": { + "type": "string" + }, + "vm": { + "type": "string" + }, + "vmApp": { + "type": "string" + } + }, + "type": "object" + }, + "body.ZoneRead": { + "properties": { + "capabilities": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": false + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "endpoints": { + "$ref": "#/components/schemas/body.ZoneEndpoints" + }, + "legacy": { + "type": "boolean" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "sys.Error": { + "properties": { + "code": { + "type": "string" + }, + "msg": { + "type": "string" + } + }, + "type": "object" + }, + "sys.ErrorResponse": { + "properties": { + "errors": { + "items": { + "$ref": "#/components/schemas/sys.Error" + }, + "type": "array", + "uniqueItems": false + } + }, + "type": "object" + } + }, + "securitySchemes": { + "ApiKeyAuth": { + "in": "header", + "name": "X-Api-Key", + "type": "apiKey" + }, + "KeycloakOAuth": { + "flows": { + "authorizationCode": { + "authorizationUrl": "https://iam.cloud.cbh.kth.se/realms/cloud/protocol/openid-connect/auth", + "tokenUrl": "https://iam.cloud.cbh.kth.se/realms/cloud/protocol/openid-connect/token" + } + }, + "in": "header", + "type": "oauth2" + } + } + }, + "info": { + "contact": { + "name": "Support", + "url": "https://github.com/kthcloud/go-deploy" + }, + "description": "This is the API explorer for the go-deploy API. You can use it as a reference for the API endpoints.", + "license": { + "name": "MIT License", + "url": "https://github.com/kthcloud/go-deploy?tab=MIT-1-ov-file#readme" + }, + "termsOfService": "http://swagger.io/terms/", + "title": "go-deploy API", + "version": "1.0" + }, + "externalDocs": { + "description": "", + "url": "" + }, + "paths": { + "/v2/deployments": { + "get": { + "description": "List deployments", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by user ID", + "in": "query", + "name": "userId", + "schema": { + "type": "string" + } + }, + { + "description": "Include shared", + "in": "query", + "name": "shared", + "schema": { + "type": "boolean" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.DeploymentRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List deployments", + "tags": [ + "Deployment" + ] + }, + "post": { + "description": "Create deployment", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.DeploymentCreate", + "summary": "body", + "description": "Deployment body" + } + ] + } + } + }, + "description": "Deployment body", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.DeploymentRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create deployment", + "tags": [ + "Deployment" + ] + } + }, + "/v2/deployments/{deploymentId}": { + "delete": { + "description": "Delete deployment", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.DeploymentCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete deployment", + "tags": [ + "Deployment" + ] + }, + "get": { + "description": "Get deployment", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.DeploymentRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get deployment", + "tags": [ + "Deployment" + ] + }, + "post": { + "description": "Update deployment", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.DeploymentUpdate", + "summary": "body", + "description": "Deployment update" + } + ] + } + } + }, + "description": "Deployment update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.DeploymentUpdated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update deployment", + "tags": [ + "Deployment" + ] + } + }, + "/v2/deployments/{deploymentId}/ciConfig": { + "get": { + "description": "Get CI config", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.CiConfig" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get CI config", + "tags": [ + "Deployment" + ] + } + }, + "/v2/deployments/{deploymentId}/command": { + "post": { + "description": "Do command", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.DeploymentCommand", + "summary": "body", + "description": "Command body" + } + ] + } + } + }, + "description": "Command body", + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Do command", + "tags": [ + "Deployment" + ] + } + }, + "/v2/deployments/{deploymentId}/logs": { + "get": { + "description": "Get logs using Server-Sent Events", + "parameters": [ + { + "description": "Deployment ID", + "in": "path", + "name": "deploymentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get logs using Server-Sent Events", + "tags": [ + "Deployment" + ] + } + }, + "/v2/discover": { + "get": { + "description": "Discover", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.DiscoverRead" + } + } + }, + "description": "OK" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Discover", + "tags": [ + "Discover" + ] + } + }, + "/v2/gpuClaims": { + "get": { + "description": "List GPU claims", + "parameters": [ + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + }, + { + "description": "Admin detailed list", + "in": "query", + "name": "detailed", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.GpuClaimRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List GPU claims", + "tags": [ + "GpuClaim" + ] + }, + "post": { + "description": "Create GpuClaim", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.GpuClaimCreate", + "summary": "body", + "description": "GpuClaim body" + } + ] + } + } + }, + "description": "GpuClaim body", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuClaimCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create GpuClaim", + "tags": [ + "GpuClaim" + ] + } + }, + "/v2/gpuClaims/{gpuClaimId}": { + "delete": { + "description": "Delete GpuClaim", + "parameters": [ + { + "description": "GpuClaim ID", + "in": "path", + "name": "gpuClaimId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuClaimCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete GpuClaim", + "tags": [ + "GpuClaim" + ] + }, + "get": { + "description": "Get GPU claim", + "parameters": [ + { + "description": "GPU claim ID", + "in": "path", + "name": "gpuClaimId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuClaimRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get GPU claim", + "tags": [ + "GpuClaim" + ] + } + }, + "/v2/gpuGroups": { + "get": { + "description": "List GPU groups", + "parameters": [ + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.GpuGroupRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List GPU groups", + "tags": [ + "GpuGroup" + ] + } + }, + "/v2/gpuGroups/{gpuGroupId}": { + "get": { + "description": "Get GPU group", + "parameters": [ + { + "description": "GPU group ID", + "in": "path", + "name": "gpuGroupId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuGroupRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get GPU group", + "tags": [ + "GpuGroup" + ] + } + }, + "/v2/gpuLeases": { + "get": { + "description": "List GPU leases", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by VM ID", + "in": "query", + "name": "vmId", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.GpuLeaseRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List GPU leases", + "tags": [ + "GpuLease" + ] + }, + "post": { + "description": "Create GPU lease", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.GpuLeaseCreate", + "summary": "body", + "description": "GPU lease" + } + ] + } + } + }, + "description": "GPU lease", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuLeaseCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create GPU Lease", + "tags": [ + "GpuLease" + ] + } + }, + "/v2/gpuLeases/{gpuLeaseId}": { + "delete": { + "description": "Delete GPU lease", + "parameters": [ + { + "description": "GPU lease ID", + "in": "path", + "name": "gpuLeaseId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuLeaseDeleted" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete GPU lease", + "tags": [ + "GpuLease" + ] + }, + "get": { + "description": "Get GPU lease", + "parameters": [ + { + "description": "GPU lease ID", + "in": "path", + "name": "gpuLeaseId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuLeaseRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get GPU lease", + "tags": [ + "GpuLease" + ] + }, + "post": { + "description": "Update GPU lease", + "parameters": [ + { + "description": "GPU lease ID", + "in": "path", + "name": "gpuLeaseId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.GpuLeaseUpdate", + "summary": "body", + "description": "GPU lease" + } + ] + } + } + }, + "description": "GPU lease", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.GpuLeaseUpdated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update GPU lease", + "tags": [ + "GpuLease" + ] + } + }, + "/v2/hooks/harbor": { + "post": { + "description": "Handle Harbor hook", + "parameters": [ + { + "description": "Basic auth token", + "in": "header", + "name": "Authorization", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.HarborWebhook", + "summary": "body", + "description": "Harbor webhook body" + } + ] + } + } + }, + "description": "Harbor webhook body", + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Handle Harbor hook", + "tags": [ + "Deployment" + ] + } + }, + "/v2/hosts": { + "get": { + "description": "List Hosts", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.HostRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List Hosts", + "tags": [ + "Host" + ] + } + }, + "/v2/hosts/verbose": { + "get": { + "description": "List Hosts verbose", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.HostVerboseRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List Hosts verbose", + "tags": [ + "Host" + ] + } + }, + "/v2/jobs": { + "get": { + "description": "List jobs", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by user ID", + "in": "query", + "name": "userId", + "schema": { + "type": "string" + } + }, + { + "description": "Filter by type", + "in": "query", + "name": "type", + "schema": { + "type": "string" + } + }, + { + "description": "Filter by status", + "in": "query", + "name": "status", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.JobRead" + }, + "type": "array" + } + } + }, + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List jobs", + "tags": [ + "Job" + ] + } + }, + "/v2/jobs/{jobId}": { + "get": { + "description": "GetJob job by id", + "parameters": [ + { + "description": "Job ID", + "in": "path", + "name": "jobId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.JobRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "GetJob job by id", + "tags": [ + "Job" + ] + }, + "post": { + "description": "Update job. Only allowed for admins.", + "parameters": [ + { + "description": "Job ID", + "in": "path", + "name": "jobId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.JobUpdate", + "summary": "body", + "description": "Job update" + } + ] + } + } + }, + "description": "Job update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.JobRead" + } + } + }, + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update job", + "tags": [ + "Job" + ] + } + }, + "/v2/metrics": { + "get": { + "description": "Get metrics", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "description": "OK" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Get metrics", + "tags": [ + "Metrics" + ] + } + }, + "/v2/notifications": { + "get": { + "description": "List notifications", + "parameters": [ + { + "description": "List all notifications", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by user ID", + "in": "query", + "name": "userId", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.NotificationRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List notifications", + "tags": [ + "Notification" + ] + } + }, + "/v2/notifications/{notificationId}": { + "delete": { + "description": "Delete notification", + "parameters": [ + { + "description": "Notification ID", + "in": "path", + "name": "notificationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete notification", + "tags": [ + "Notification" + ] + }, + "get": { + "description": "Get notification", + "parameters": [ + { + "description": "Notification ID", + "in": "path", + "name": "notificationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.NotificationRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get notification", + "tags": [ + "Notification" + ] + }, + "post": { + "description": "Update notification", + "parameters": [ + { + "description": "Notification ID", + "in": "path", + "name": "notificationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.NotificationUpdate", + "summary": "body", + "description": "Notification update" + } + ] + } + } + }, + "description": "Notification update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update notification", + "tags": [ + "Notification" + ] + } + }, + "/v2/register": { + "get": { + "description": "Register resource", + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Register resource", + "tags": [ + "Register" + ] + } + }, + "/v2/resourceMigrations": { + "get": { + "description": "List resource migrations", + "parameters": [ + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.ResourceMigrationRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List resource migrations", + "tags": [ + "ResourceMigration" + ] + }, + "post": { + "description": "Create resource migration", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.ResourceMigrationCreate", + "summary": "body", + "description": "Resource Migration Create" + } + ] + } + } + }, + "description": "Resource Migration Create", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.ResourceMigrationCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create resource migration", + "tags": [ + "ResourceMigration" + ] + } + }, + "/v2/resourceMigrations/{resourceMigrationId}": { + "delete": { + "description": "Delete resource migration", + "parameters": [ + { + "description": "Resource Migration ID", + "in": "path", + "name": "resourceMigrationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete resource migration", + "tags": [ + "ResourceMigration" + ] + }, + "get": { + "description": "Get resource migration", + "parameters": [ + { + "description": "Resource Migration ID", + "in": "path", + "name": "resourceMigrationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.ResourceMigrationRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get resource migration", + "tags": [ + "ResourceMigration" + ] + }, + "post": { + "description": "Update resource migration", + "parameters": [ + { + "description": "Resource Migration ID", + "in": "path", + "name": "resourceMigrationId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.ResourceMigrationUpdate", + "summary": "body", + "description": "Resource Migration Update" + } + ] + } + } + }, + "description": "Resource Migration Update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.ResourceMigrationUpdated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update resource migration", + "tags": [ + "ResourceMigration" + ] + } + }, + "/v2/snapshots/{vmId}": { + "get": { + "description": "List snapshots", + "parameters": [ + { + "description": "Filter by VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.VmSnapshotRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "423": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Locked" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List snapshots", + "tags": [ + "Snapshot" + ] + }, + "post": { + "description": "Create snapshot", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmSnapshotCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create snapshot", + "tags": [ + "Snapshot" + ] + } + }, + "/v2/storageManagers": { + "get": { + "description": "Get storage manager list", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.SmRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get storage manager list", + "tags": [ + "StorageManager" + ] + } + }, + "/v2/storageManagers/{storageManagerId}": { + "delete": { + "description": "Delete storage manager", + "parameters": [ + { + "description": "Storage manager ID", + "in": "path", + "name": "storageManagerId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.SmDeleted" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete storage manager", + "tags": [ + "StorageManager" + ] + }, + "get": { + "description": "Get storage manager", + "parameters": [ + { + "description": "Storage manager ID", + "in": "path", + "name": "storageManagerId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.SmDeleted" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get storage manager", + "tags": [ + "StorageManager" + ] + } + }, + "/v2/systemCapacities": { + "get": { + "description": "List system capacities", + "parameters": [ + { + "description": "n", + "in": "query", + "name": "n", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.TimestampedSystemCapacities" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List system capacities", + "tags": [ + "System" + ] + } + }, + "/v2/systemStats": { + "get": { + "description": "List system stats", + "parameters": [ + { + "description": "n", + "in": "query", + "name": "n", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.TimestampedSystemStats" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List system stats", + "tags": [ + "System" + ] + } + }, + "/v2/systemStatus": { + "get": { + "description": "List system stats", + "parameters": [ + { + "description": "n", + "in": "query", + "name": "n", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.TimestampedSystemStatus" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List system stats", + "tags": [ + "System" + ] + } + }, + "/v2/teams": { + "get": { + "description": "List teams", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by user ID", + "in": "query", + "name": "userId", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.TeamRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List teams", + "tags": [ + "Team" + ] + }, + "post": { + "description": "Create team", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.TeamCreate", + "summary": "body", + "description": "Team" + } + ] + } + } + }, + "description": "Team", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.TeamRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create team", + "tags": [ + "Team" + ] + } + }, + "/v2/teams/{teamId}": { + "delete": { + "description": "Delete team", + "parameters": [ + { + "description": "Team ID", + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete team", + "tags": [ + "Team" + ] + }, + "get": { + "description": "Get team", + "parameters": [ + { + "description": "Team ID", + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.TeamRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get team", + "tags": [ + "Team" + ] + }, + "post": { + "description": "Update team", + "parameters": [ + { + "description": "Team ID", + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.TeamUpdate", + "summary": "body", + "description": "Team" + } + ] + } + } + }, + "description": "Team", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.TeamRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.BindingError" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update team", + "tags": [ + "Team" + ] + } + }, + "/v2/users": { + "get": { + "description": "List users", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Discovery mode", + "in": "query", + "name": "discover", + "schema": { + "type": "boolean" + } + }, + { + "description": "Search query", + "in": "query", + "name": "search", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.UserRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List users", + "tags": [ + "User" + ] + } + }, + "/v2/users/{userId}": { + "get": { + "description": "Get user", + "parameters": [ + { + "description": "User ID", + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Discovery mode", + "in": "query", + "name": "discover", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.UserRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get user", + "tags": [ + "User" + ] + }, + "post": { + "description": "Update user", + "parameters": [ + { + "description": "User ID", + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.UserUpdate", + "summary": "body", + "description": "User update" + } + ] + } + } + }, + "description": "User update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.UserRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update user", + "tags": [ + "User" + ] + } + }, + "/v2/users/{userId}/apiKeys": { + "post": { + "description": "Create API key", + "parameters": [ + { + "description": "User ID", + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.ApiKeyCreate", + "summary": "body", + "description": "API key create body" + } + ] + } + } + }, + "description": "API key create body", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.ApiKeyCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create API key", + "tags": [ + "User" + ] + } + }, + "/v2/vmActions/{vmId}": { + "post": { + "description": "Creates a new action", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.VmActionCreate", + "summary": "body", + "description": "actions body" + } + ] + } + } + }, + "description": "actions body", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmActionCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Creates a new action", + "tags": [ + "VmAction" + ] + } + }, + "/v2/vms": { + "get": { + "description": "List VMs", + "parameters": [ + { + "description": "List all", + "in": "query", + "name": "all", + "schema": { + "type": "boolean" + } + }, + { + "description": "Filter by user ID", + "in": "query", + "name": "userId", + "schema": { + "type": "string" + } + }, + { + "description": "Page number", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "Number of items per page", + "in": "query", + "name": "pageSize", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.VmRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List VMs", + "tags": [ + "VM" + ] + }, + "post": { + "description": "Create VM", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.VmCreate", + "summary": "body", + "description": "VM body" + } + ] + } + } + }, + "description": "VM body", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmCreated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Create VM", + "tags": [ + "VM" + ] + } + }, + "/v2/vms/{vmId}": { + "delete": { + "description": "Delete VM", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmDeleted" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete VM", + "tags": [ + "VM" + ] + }, + "get": { + "description": "Get VM", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get VM", + "tags": [ + "VM" + ] + }, + "post": { + "description": "Update VM", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/body.VmUpdate", + "summary": "body", + "description": "VM update" + } + ] + } + } + }, + "description": "VM update", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmUpdated" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Update VM", + "tags": [ + "VM" + ] + } + }, + "/v2/vms/{vmId}/snapshot/{snapshotId}": { + "delete": { + "description": "Delete snapshot", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Snapshot ID", + "in": "path", + "name": "snapshotId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmSnapshotDeleted" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Delete snapshot", + "tags": [ + "Snapshot" + ] + }, + "post": { + "description": "Get snapshot", + "parameters": [ + { + "description": "VM ID", + "in": "path", + "name": "vmId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Snapshot ID", + "in": "path", + "name": "snapshotId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/body.VmSnapshotRead" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "Get snapshot", + "tags": [ + "Snapshot" + ] + } + }, + "/v2/workerStatus": { + "get": { + "description": "List of worker status", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.WorkerStatusRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List worker status", + "tags": [ + "Status" + ] + } + }, + "/v2/zones": { + "get": { + "description": "List zones", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/body.ZoneRead" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sys.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "KeycloakOAuth": [] + } + ], + "summary": "List zones", + "tags": [ + "Zone" + ] + } + } + }, "openapi": "3.1.0" } \ No newline at end of file diff --git a/docs/api/v2/V2_swagger.yaml b/docs/api/v2/V2_swagger.yaml index 1003b712..bcb3bb07 100644 --- a/docs/api/v2/V2_swagger.yaml +++ b/docs/api/v2/V2_swagger.yaml @@ -1,5 +1,16 @@ components: schemas: + body.AllocatedGpu: + properties: + adminAccess: + type: boolean + device: + type: string + pool: + type: string + shareID: + type: string + type: object body.ApiKey: properties: createdAt: @@ -142,6 +153,12 @@ components: minItems: 0 type: array uniqueItems: false + gpus: + items: + $ref: '#/components/schemas/body.DeploymentGPU' + minItems: 0 + type: array + uniqueItems: false healthCheckPath: maxLength: 1000 minLength: 0 @@ -202,6 +219,16 @@ components: jobId: type: string type: object + body.DeploymentGPU: + properties: + claimName: + type: string + name: + type: string + required: + - claimName + - name + type: object body.DeploymentRead: properties: accessedAt: @@ -297,6 +324,11 @@ components: properties: cpuCores: type: number + gpus: + items: + $ref: '#/components/schemas/body.DeploymentGPU' + type: array + uniqueItems: false ram: type: number replicas: @@ -325,6 +357,12 @@ components: minItems: 0 type: array uniqueItems: false + gpus: + items: + $ref: '#/components/schemas/body.DeploymentGPU' + minItems: 0 + type: array + uniqueItems: false healthCheckPath: maxLength: 1000 minLength: 0 @@ -407,6 +445,106 @@ components: total: type: integer type: object + body.GpuClaimConsumer: + properties: + apiGroup: + type: string + name: + type: string + resource: + type: string + uid: + type: string + type: object + body.GpuClaimCreate: + properties: + allowedRoles: + items: + type: string + type: array + uniqueItems: false + name: + maxLength: 30 + minLength: 3 + type: string + requested: + description: Requested contains all requested GPU configurations by key + (request.Name). + items: + $ref: '#/components/schemas/body.RequestedGpuCreate' + minItems: 1 + type: array + uniqueItems: false + zone: + type: string + required: + - name + type: object + body.GpuClaimCreated: + properties: + id: + type: string + jobId: + type: string + type: object + body.GpuClaimRead: + properties: + allocated: + additionalProperties: + items: + $ref: '#/components/schemas/body.AllocatedGpu' + type: array + description: Allocated contains the GPUs that have been successfully bound/allocated. + type: object + allowedRoles: + description: Roles allowed to use this GpuClaim, empty means all + items: + type: string + type: array + uniqueItems: false + consumers: + description: Consumers are the workloads currently using this claim. + items: + $ref: '#/components/schemas/body.GpuClaimConsumer' + type: array + uniqueItems: false + createdAt: + type: string + id: + type: string + lastError: + description: LastError holds the last reconciliation or provisioning error + message. + type: string + name: + type: string + requested: + additionalProperties: + $ref: '#/components/schemas/body.RequestedGpu' + description: Requested contains all requested GPU configurations by key + (request.Name). + type: object + status: + $ref: '#/components/schemas/body.GpuClaimStatus' + updatedAt: + type: string + zone: + type: string + type: object + body.GpuClaimStatus: + description: Status reflects the reconciliation and/or lifecycle state. + properties: + lastSynced: + type: string + message: + type: string + phase: + type: string + updatedAt: + type: string + type: object + body.GpuDeviceConfigurationWrapper: + type: object body.GpuGroupRead: properties: available: @@ -564,67 +702,38 @@ components: properties: cpuCore: $ref: '#/components/schemas/body.CpuCoreCapacities' - displayName: - type: string gpu: $ref: '#/components/schemas/body.GpuCapacities' - name: - type: string ram: $ref: '#/components/schemas/body.RamCapacities' - zone: - description: Zone is the name of the zone where the host is located. - type: string type: object body.HostRead: - properties: - displayName: - type: string - name: - type: string - zone: - description: Zone is the name of the zone where the host is located. - type: string type: object body.HostStatus: properties: cpu: $ref: '#/components/schemas/body.CpuStatus' - displayName: - type: string gpu: $ref: '#/components/schemas/body.GpuStatus' - name: - type: string ram: $ref: '#/components/schemas/body.RamStatus' - zone: - description: Zone is the name of the zone where the host is located. - type: string type: object body.HostVerboseRead: properties: deactivatedUntil: type: string - displayName: - type: string enabled: type: boolean ip: type: string lastSeenAt: type: string - name: - type: string port: type: integer registeredAt: type: string schedulable: type: boolean - zone: - description: Zone is the name of the zone where the host is located. - type: string type: object body.HttpProxyCreate: properties: @@ -811,6 +920,8 @@ components: gpuLeaseDuration: description: in hours type: number + gpus: + type: integer ram: type: number snapshots: @@ -847,6 +958,41 @@ components: description: UnavailableReplicas is the number of replicas that are unavailable. type: integer type: object + body.RequestedGpu: + properties: + allocationMode: + enum: + - All + - ExactCount + type: string + capacity: + additionalProperties: + type: string + type: object + config: + $ref: '#/components/schemas/body.GpuDeviceConfigurationWrapper' + count: + type: integer + deviceClassName: + type: string + selectors: + items: + type: string + type: array + uniqueItems: false + required: + - allocationMode + - deviceClassName + type: object + body.RequestedGpuCreate: + properties: + name: + maxLength: 30 + minLength: 3 + type: string + required: + - name + type: object body.ResourceMigrationCreate: properties: resourceId: @@ -889,53 +1035,11 @@ components: type: object body.ResourceMigrationCreated: properties: - createdAt: - type: string - deletedAt: - type: string - id: - type: string jobId: description: |- JobID is the ID of the job that was created for the resource migration. It will only be set if the migration was created with status 'accepted'. type: string - resourceId: - description: |- - ResourceID is the ID of the resource that is being migrated. - This can be a VM ID, deployment ID, etc. depending on the type of the migration. - type: string - resourceType: - description: |- - ResourceType is the type of the resource that is being migrated. - - Possible values: - - vm - - deployment - type: string - status: - description: |- - Status is the status of the resource migration. - When this field is set to 'accepted', the migration will take place and then automatically be deleted. - type: string - type: - description: |- - Type is the type of the resource migration. - - Possible values: - - updateOwner - type: string - updateOwner: - description: |- - UpdateOwner is the set of parameters that are required for the updateOwner migration type. - It is empty if the migration type is not updateOwner. - properties: - ownerId: - type: string - type: object - userId: - description: UserID is the ID of the user who initiated the migration. - type: string type: object body.ResourceMigrationRead: properties: @@ -1004,53 +1108,11 @@ components: type: object body.ResourceMigrationUpdated: properties: - createdAt: - type: string - deletedAt: - type: string - id: - type: string jobId: description: |- JobID is the ID of the job that was created for the resource migration. It will only be set if the migration was updated with status 'accepted'. type: string - resourceId: - description: |- - ResourceID is the ID of the resource that is being migrated. - This can be a VM ID, deployment ID, etc. depending on the type of the migration. - type: string - resourceType: - description: |- - ResourceType is the type of the resource that is being migrated. - - Possible values: - - vm - - deployment - type: string - status: - description: |- - Status is the status of the resource migration. - When this field is set to 'accepted', the migration will take place and then automatically be deleted. - type: string - type: - description: |- - Type is the type of the resource migration. - - Possible values: - - updateOwner - type: string - updateOwner: - description: |- - UpdateOwner is the set of parameters that are required for the updateOwner migration type. - It is empty if the migration type is not updateOwner. - properties: - ownerId: - type: string - type: object - userId: - description: UserID is the ID of the user who initiated the migration. - type: string type: object body.Role: properties: @@ -1150,24 +1212,12 @@ components: properties: addedAt: type: string - email: - type: string - firstName: - type: string - gravatarUrl: - type: string - id: - type: string joinedAt: type: string - lastName: - type: string memberStatus: type: string teamRole: type: string - username: - type: string type: object body.TeamMemberCreate: properties: @@ -1274,6 +1324,8 @@ components: type: number diskSize: type: integer + gpus: + type: integer ram: type: number type: object @@ -1727,7 +1779,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.DeploymentCreate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.DeploymentCreate' + description: Deployment body + summary: body description: Deployment body required: true responses: @@ -1872,7 +1928,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.DeploymentUpdate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.DeploymentUpdate' + description: Deployment update + summary: body description: Deployment update required: true responses: @@ -1961,7 +2021,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.DeploymentCommand' + oneOf: + - type: object + - $ref: '#/components/schemas/body.DeploymentCommand' + description: Command body + summary: body description: Command body required: true responses: @@ -2057,6 +2121,213 @@ paths: summary: Discover tags: - Discover + /v2/gpuClaims: + get: + description: List GPU claims + parameters: + - description: Page number + in: query + name: page + schema: + type: integer + - description: Number of items per page + in: query + name: pageSize + schema: + type: integer + - description: Admin detailed list + in: query + name: detailed + schema: + type: boolean + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/body.GpuClaimRead' + type: array + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Bad Request + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Not Found + "423": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Locked + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Internal Server Error + security: + - ApiKeyAuth: [] + - KeycloakOAuth: [] + summary: List GPU claims + tags: + - GpuClaim + post: + description: Create GpuClaim + requestBody: + content: + application/json: + schema: + oneOf: + - type: object + - $ref: '#/components/schemas/body.GpuClaimCreate' + description: GpuClaim body + summary: body + description: GpuClaim body + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/body.GpuClaimCreated' + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Bad Request + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Forbidden + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Not Found + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Internal Server Error + security: + - ApiKeyAuth: [] + - KeycloakOAuth: [] + summary: Create GpuClaim + tags: + - GpuClaim + /v2/gpuClaims/{gpuClaimId}: + delete: + description: Delete GpuClaim + parameters: + - description: GpuClaim ID + in: path + name: gpuClaimId + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/body.GpuClaimCreated' + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Bad Request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Forbidden + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Not Found + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Internal Server Error + security: + - ApiKeyAuth: [] + - KeycloakOAuth: [] + summary: Delete GpuClaim + tags: + - GpuClaim + get: + description: Get GPU claim + parameters: + - description: GPU claim ID + in: path + name: gpuClaimId + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/body.GpuClaimRead' + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Bad Request + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Not Found + "423": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Locked + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/sys.ErrorResponse' + description: Internal Server Error + security: + - ApiKeyAuth: [] + - KeycloakOAuth: [] + summary: Get GPU claim + tags: + - GpuClaim /v2/gpuGroups: get: description: List GPU groups @@ -2226,7 +2497,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.GpuLeaseCreate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.GpuLeaseCreate' + description: GPU lease + summary: body description: GPU lease required: true responses: @@ -2365,7 +2640,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.GpuLeaseUpdate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.GpuLeaseUpdate' + description: GPU lease + summary: body description: GPU lease required: true responses: @@ -2412,7 +2691,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.HarborWebhook' + oneOf: + - type: object + - $ref: '#/components/schemas/body.HarborWebhook' + description: Harbor webhook body + summary: body description: Harbor webhook body required: true responses: @@ -2615,7 +2898,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.JobUpdate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.JobUpdate' + description: Job update + summary: body description: Job update required: true responses: @@ -2780,7 +3067,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.NotificationUpdate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.NotificationUpdate' + description: Notification update + summary: body description: Notification update required: true responses: @@ -2882,7 +3173,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.ResourceMigrationCreate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.ResourceMigrationCreate' + description: Resource Migration Create + summary: body description: Resource Migration Create required: true responses: @@ -3011,7 +3306,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.ResourceMigrationUpdate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.ResourceMigrationUpdate' + description: Resource Migration Update + summary: body description: Resource Migration Update required: true responses: @@ -3045,7 +3344,7 @@ paths: summary: Update resource migration tags: - ResourceMigration - /v2/snapshots: + /v2/snapshots/{vmId}: get: description: List snapshots parameters: @@ -3417,11 +3716,6 @@ paths: name: pageSize schema: type: integer - requestBody: - content: - application/json: - schema: - type: object responses: "200": content: @@ -3455,7 +3749,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.TeamCreate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.TeamCreate' + description: Team + summary: body description: Team required: true responses: @@ -3528,11 +3826,6 @@ paths: required: true schema: type: string - requestBody: - content: - application/json: - schema: - type: object responses: "200": content: @@ -3571,7 +3864,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.TeamUpdate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.TeamUpdate' + description: Team + summary: body description: Team required: true responses: @@ -3708,7 +4005,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.UserUpdate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.UserUpdate' + description: User update + summary: body description: User update required: true responses: @@ -3750,7 +4051,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.ApiKeyCreate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.ApiKeyCreate' + description: API key create body + summary: body description: API key create body required: true responses: @@ -3778,7 +4083,7 @@ paths: summary: Create API key tags: - User - /v2/vmActions: + /v2/vmActions/{vmId}: post: description: Creates a new action parameters: @@ -3792,7 +4097,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.VmActionCreate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.VmActionCreate' + description: actions body + summary: body description: actions body required: true responses: @@ -3895,7 +4204,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.VmCreate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.VmCreate' + description: VM body + summary: body description: VM body required: true responses: @@ -4046,7 +4359,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/body.VmUpdate' + oneOf: + - type: object + - $ref: '#/components/schemas/body.VmUpdate' + description: VM update + summary: body description: VM update required: true responses: diff --git a/dto/v2/body/deployment.go b/dto/v2/body/deployment.go index 2151fc51..cf702b7f 100644 --- a/dto/v2/body/deployment.go +++ b/dto/v2/body/deployment.go @@ -53,9 +53,10 @@ type DeploymentRead struct { type DeploymentCreate struct { Name string `json:"name" bson:"name" binding:"required,rfc1035,min=3,max=30,deployment_name"` - CpuCores *float64 `json:"cpuCores,omitempty" bson:"cpuCores,omitempty" binding:"omitempty,min=0.1"` - RAM *float64 `json:"ram,omitempty" bson:"ram,omitempty" binding:"omitempty,min=0.1"` - Replicas *int `json:"replicas,omitempty" bson:"replicas,omitempty" binding:"omitempty,min=0,max=100"` + CpuCores *float64 `json:"cpuCores,omitempty" bson:"cpuCores,omitempty" binding:"omitempty,min=0.1"` + RAM *float64 `json:"ram,omitempty" bson:"ram,omitempty" binding:"omitempty,min=0.1"` + Replicas *int `json:"replicas,omitempty" bson:"replicas,omitempty" binding:"omitempty,min=0,max=100"` + GPUs []DeploymentGPU `json:"gpus,omitempty" bson:"gpus,omitempty" binding:"omitempty,min=0"` Envs []Env `json:"envs" bson:"envs" binding:"omitempty,env_list,min=0,max=1000,dive"` Volumes []Volume `json:"volumes" bson:"volumes" binding:"omitempty,min=0,max=100,dive"` @@ -83,9 +84,10 @@ type DeploymentCreate struct { type DeploymentUpdate struct { Name *string `json:"name,omitempty" bson:"name,omitempty" binding:"omitempty,required,rfc1035,min=3,max=30,deployment_name"` - CpuCores *float64 `json:"cpuCores,omitempty" bson:"cpuCores,omitempty" binding:"omitempty,min=0.1"` - RAM *float64 `json:"ram,omitempty" bson:"ram,omitempty" binding:"omitempty,min=0.1"` - Replicas *int `json:"replicas,omitempty" bson:"replicas,omitempty" binding:"omitempty,min=0,max=100"` + CpuCores *float64 `json:"cpuCores,omitempty" bson:"cpuCores,omitempty" binding:"omitempty,min=0.1"` + RAM *float64 `json:"ram,omitempty" bson:"ram,omitempty" binding:"omitempty,min=0.1"` + Replicas *int `json:"replicas,omitempty" bson:"replicas,omitempty" binding:"omitempty,min=0,max=100"` + GPUs *[]DeploymentGPU `json:"gpus,omitempty" bson:"gpus,omitempty" binding:"omitempty,min=0"` Envs *[]Env `json:"envs,omitempty" bson:"envs,omitempty" binding:"omitempty,env_list,min=0,max=1000,dive"` Volumes *[]Volume `json:"volumes,omitempty" bson:"volumes,omitempty" binding:"omitempty,min=0,max=100,dive"` @@ -149,10 +151,16 @@ type DeploymentUpdated struct { JobID *string `json:"jobId,omitempty"` } +type DeploymentGPU struct { + Name string `json:"name,omitempty" binding:"required"` + ClaimName string `json:"claimName,omitempty" binding:"required"` +} + type DeploymentSpecs struct { - CpuCores float64 `json:"cpuCores"` - RAM float64 `json:"ram"` - Replicas int `json:"replicas"` + CpuCores float64 `json:"cpuCores"` + RAM float64 `json:"ram"` + Replicas int `json:"replicas"` + GPUs []DeploymentGPU `json:"gpus,omitempty"` } type CiConfig struct { diff --git a/dto/v2/body/gpu_claim.go b/dto/v2/body/gpu_claim.go new file mode 100644 index 00000000..6e2a8cfd --- /dev/null +++ b/dto/v2/body/gpu_claim.go @@ -0,0 +1,244 @@ +package body + +import ( + "encoding/json" + "time" + + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/api/nvidia" + "go.mongodb.org/mongo-driver/bson" +) + +// GpuClaimRead is a detailed DTO for administrators +// providing full visibility into requested, allocated, +// and consumed GPU resources. +type GpuClaimRead struct { + ID string `json:"id"` + Name string `json:"name"` + Zone string `json:"zone"` + + // Roles allowed to use this GpuClaim, empty means all + AllowedRoles []string `json:"allowedRoles,omitempty"` + + // Requested contains all requested GPU configurations by key (request.Name). + Requested map[string]RequestedGpu `json:"requested,omitempty"` + + // Allocated contains the GPUs that have been successfully bound/allocated. + Allocated map[string][]AllocatedGpu `json:"allocated,omitempty"` + + // Consumers are the workloads currently using this claim. + Consumers []GpuClaimConsumer `json:"consumers,omitempty"` + + // Status reflects the reconciliation and/or lifecycle state. + Status *GpuClaimStatus `json:"status,omitempty"` + + // LastError holds the last reconciliation or provisioning error message. + LastError string `json:"lastError,omitempty"` + + CreatedAt time.Time `json:"createdAt"` + UpdatedAt *time.Time `json:"updatedAt,omitempty"` +} + +type GpuClaimCreate struct { + Name string `json:"name" bson:"name" binding:"required,rfc1035,min=3,max=30"` + Zone *string `json:"zone" bson:"zone"` + + AllowedRoles []string `json:"allowedRoles,omitempty" bson:"allowedRoles,omitempty"` + + // Requested contains all requested GPU configurations by key (request.Name). + Requested []RequestedGpuCreate `json:"requested,omitempty" bson:"requested,omitempty" binding:"min=1,dive"` +} + +type GpuClaimCreated struct { + ID string `json:"id"` + JobID string `json:"jobId"` +} + +type RequestedGpuCreate struct { + RequestedGpu `mapstructure:",squash" json:",inline" bson:",inline" binding:"required"` + Name string `json:"name" bson:"name" binding:"required,rfc1035,min=3,max=30"` +} + +// RequestedGpu describes the desired GPU configuration that was requested. +type RequestedGpu struct { + AllocationMode string `json:"allocationMode" bson:"allocationMode" binding:"required,oneof=All ExactCount"` + Capacity map[string]string `json:"capacity,omitempty" bson:"capacity,omitempty"` + Count *int64 `json:"count,omitempty" bson:"count,omitempty"` + DeviceClassName string `json:"deviceClassName" bson:"deviceClassName" binding:"required,rfc1123"` + Selectors []string `json:"selectors,omitempty" bson:"selectors,omitempty"` + Config *GpuDeviceConfigurationWrapper `json:"config,omitempty" bson:"config,omitempty"` +} + +type GpuDeviceConfigurationWrapper struct { + GpuDeviceConfiguration `mapstructure:"-" json:"-" bson:"-"` +} + +func (w *GpuDeviceConfigurationWrapper) UnmarshalJSON(data []byte) error { + // Peek at the "driver" field first + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + var driver string + if d, ok := raw["driver"]; ok { + if err := json.Unmarshal(d, &driver); err != nil { + return err + } + } + + switch driver { + case "gpu.nvidia.com": + var n NvidiaDeviceConfiguration + if err := json.Unmarshal(data, &n); err != nil { + return err + } + if n.Parameters != nil { + if n.Parameters.APIVersion == "" { + n.Parameters.APIVersion = "resource.nvidia.com/v1beta1" + } + if n.Parameters.Kind == "" { + n.Parameters.Kind = "GpuConfig" + } + } + w.GpuDeviceConfiguration = n + + default: + var g GenericDeviceConfiguration + if err := json.Unmarshal(data, &g); err != nil { + return err + } + w.GpuDeviceConfiguration = g + } + + return nil +} + +func (w GpuDeviceConfigurationWrapper) MarshalJSON() ([]byte, error) { + if w.GpuDeviceConfiguration == nil { + return []byte("null"), nil + } + return json.Marshal(w.GpuDeviceConfiguration) +} + +// MarshalBSON implements bson.Marshaler +func (w GpuDeviceConfigurationWrapper) MarshalBSON() ([]byte, error) { + j, err := json.Marshal(w) + if err != nil { + return nil, err + } + var doc map[string]any + if err := bson.UnmarshalExtJSON(j, false, &doc); err != nil { + return nil, err + } + return bson.Marshal(doc) +} + +// UnmarshalBSON implements bson.Unmarshaler +func (w *GpuDeviceConfigurationWrapper) UnmarshalBSON(data []byte) error { + var raw map[string]interface{} + if err := bson.Unmarshal(data, &raw); err != nil { + return err + } + + // Detect driver + driver := "" + if d, ok := raw["driver"].(string); ok { + driver = d + } + + switch driver { + case "gpu.nvidia.com": + var n NvidiaDeviceConfiguration + if err := bson.Unmarshal(data, &n); err != nil { + return err + } + if n.Parameters != nil { + if n.Parameters.APIVersion == "" { + n.Parameters.APIVersion = "resource.nvidia.com/v1beta1" + } + if n.Parameters.Kind == "" { + n.Parameters.Kind = "GpuConfig" + } + } + w.GpuDeviceConfiguration = n + default: + var g GenericDeviceConfiguration + if err := bson.Unmarshal(data, &g); err != nil { + return err + } + w.GpuDeviceConfiguration = g + } + + return nil +} + +// GpuDeviceConfiguration represents a vendor-specific GPU configuration. +type GpuDeviceConfiguration interface { + DriverName() string + json.Marshaler +} + +// GenericDeviceConfiguration is a catch-all configuration when no vendor-specific struct is used. +type GenericDeviceConfiguration struct { + Driver string `json:"driver" bson:"driver"` +} + +func (g GenericDeviceConfiguration) DriverName() string { + return g.Driver +} + +func (g GenericDeviceConfiguration) MarshalJSON() ([]byte, error) { + type genericAlias GenericDeviceConfiguration + return json.Marshal(&struct { + Type string `json:"type" bson:"type"` + Alias genericAlias `json:",inline" bson:",inline"` + }{ + Type: "generic", + Alias: (genericAlias)(g), + }) +} + +// NvidiaDeviceConfiguration represents NVIDIA-specific configuration options. +type NvidiaDeviceConfiguration struct { + Driver string `json:"driver" bson:"driver"` + Parameters *nvidia.GpuConfig `json:"parameters,omitempty" bson:"parameters,omitempty"` +} + +func (NvidiaDeviceConfiguration) DriverName() string { + return "gpu.nvidia.com" +} + +func (n NvidiaDeviceConfiguration) MarshalJSON() ([]byte, error) { + type nvidiaAlias NvidiaDeviceConfiguration + return json.Marshal(&struct { + Type string `json:"type" bson:"type"` + Alias nvidiaAlias `json:",inline" bson:",inline"` + }{ + Type: "nvidia", + Alias: (nvidiaAlias)(n), + }) +} + +// AllocatedGpu represents a concrete allocated GPU or GPU share. +type AllocatedGpu struct { + Pool string `json:"pool,omitempty"` + Device string `json:"device,omitempty"` + ShareID string `json:"shareID,omitempty"` + AdminAccess bool `json:"adminAccess,omitempty"` +} + +// GpuClaimConsumer describes a workload consuming this GPU claim. +type GpuClaimConsumer struct { + APIGroup string `json:"apiGroup,omitempty"` + Resource string `json:"resource,omitempty"` + Name string `json:"name,omitempty"` + UID string `json:"uid,omitempty"` +} + +// GpuClaimStatus represents runtime state and metadata about allocation progress. +type GpuClaimStatus struct { + Phase string `json:"phase,omitempty"` + Message string `json:"message,omitempty"` + UpdatedAt *time.Time `json:"updatedAt,omitempty"` + LastSynced *time.Time `json:"lastSynced,omitempty"` +} diff --git a/dto/v2/body/user.go b/dto/v2/body/user.go index dd841cb3..9d33eb1f 100644 --- a/dto/v2/body/user.go +++ b/dto/v2/body/user.go @@ -62,10 +62,12 @@ type Quota struct { DiskSize float64 `json:"diskSize"` Snapshots int `json:"snapshots"` GpuLeaseDuration float64 `json:"gpuLeaseDuration"` // in hours + Gpus int `json:"gpus"` } type Usage struct { CpuCores float64 `json:"cpuCores"` RAM float64 `json:"ram"` DiskSize int `json:"diskSize"` + Gpus int `json:"gpus"` } diff --git a/dto/v2/query/gpu_claim.go b/dto/v2/query/gpu_claim.go new file mode 100644 index 00000000..a1279338 --- /dev/null +++ b/dto/v2/query/gpu_claim.go @@ -0,0 +1,6 @@ +package query + +type GpuClaimList struct { + *Pagination + Detailed bool `form:"detailed"` +} diff --git a/dto/v2/uri/gpu_claim.go b/dto/v2/uri/gpu_claim.go new file mode 100644 index 00000000..16c9777d --- /dev/null +++ b/dto/v2/uri/gpu_claim.go @@ -0,0 +1,12 @@ +package uri + +type GpuClaimList struct { +} + +type GpuClaimGet struct { + GpuClaimID string `uri:"gpuClaimId" binding:"required"` +} + +type GpuClaimDelete struct { + GpuClaimID string `uri:"gpuClaimId" binding:"required"` +} diff --git a/export/types/v2/body/index.ts b/export/types/v2/body/index.ts index 062e99a6..241d925c 100644 --- a/export/types/v2/body/index.ts +++ b/export/types/v2/body/index.ts @@ -68,6 +68,7 @@ export interface DeploymentCreate { cpuCores?: number /* float64 */; ram?: number /* float64 */; replicas?: number /* int */; + gpus?: DeploymentGPU[]; envs: Env[]; volumes: Volume[]; initCommands: string[]; @@ -99,6 +100,7 @@ export interface DeploymentUpdate { cpuCores?: number /* float64 */; ram?: number /* float64 */; replicas?: number /* int */; + gpus?: DeploymentGPU[]; envs?: Env[]; volumes?: Volume[]; initCommands?: string[]; @@ -162,10 +164,15 @@ export interface DeploymentUpdated { id: string; jobId?: string; } +export interface DeploymentGPU { + name?: string; + claimName?: string; +} export interface DeploymentSpecs { cpuCores: number /* float64 */; ram: number /* float64 */; replicas: number /* int */; + gpus?: DeploymentGPU[]; } export interface CiConfig { config: string; @@ -195,6 +202,121 @@ export interface BindingError { validationErrors: { [key: string]: string[]}; } +////////// +// source: gpu_claim.go + +/** + * GpuClaimRead is a detailed DTO for administrators + * providing full visibility into requested, allocated, + * and consumed GPU resources. + */ +export interface GpuClaimRead { + id: string; + name: string; + zone: string; + /** + * Roles allowed to use this GpuClaim, empty means all + */ + allowedRoles?: string[]; + /** + * Requested contains all requested GPU configurations by key (request.Name). + */ + requested?: { [key: string]: RequestedGpu}; + /** + * Allocated contains the GPUs that have been successfully bound/allocated. + */ + allocated?: { [key: string]: AllocatedGpu[]}; + /** + * Consumers are the workloads currently using this claim. + */ + consumers?: GpuClaimConsumer[]; + /** + * Status reflects the reconciliation and/or lifecycle state. + */ + status?: GpuClaimStatus; + /** + * LastError holds the last reconciliation or provisioning error message. + */ + lastError?: string; + createdAt: string; + updatedAt?: string; +} +export interface GpuClaimCreate { + name: string; + zone?: string; + allowedRoles?: string[]; + /** + * Requested contains all requested GPU configurations by key (request.Name). + */ + requested?: RequestedGpuCreate[]; +} +export interface GpuClaimCreated { + id: string; + jobId: string; +} +export interface RequestedGpuCreate { + RequestedGpu: RequestedGpu; + name: string; +} +/** + * RequestedGpu describes the desired GPU configuration that was requested. + */ +export interface RequestedGpu { + allocationMode: string; + capacity?: { [key: string]: string}; + count?: number /* int64 */; + deviceClassName: string; + selectors?: string[]; + config?: GpuDeviceConfigurationWrapper; +} +export interface GpuDeviceConfigurationWrapper { +} +/** + * GpuDeviceConfiguration represents a vendor-specific GPU configuration. + */ +export type GpuDeviceConfiguration = + any /* json.Marshaler */; +/** + * GenericDeviceConfiguration is a catch-all configuration when no vendor-specific struct is used. + */ +export interface GenericDeviceConfiguration { + driver: string; +} +/** + * NvidiaDeviceConfiguration represents NVIDIA-specific configuration options. + */ +export interface NvidiaDeviceConfiguration { + driver: string; + parameters?: any /* nvidia.GpuConfig */; +} +/** + * AllocatedGpu represents a concrete allocated GPU or GPU share. + */ +export interface AllocatedGpu { + pool?: string; + device?: string; + shareID?: string; + adminAccess?: boolean; +} +/** + * GpuClaimConsumer describes a workload consuming this GPU claim. + */ +export interface GpuClaimConsumer { + apiGroup?: string; + resource?: string; + name?: string; + uid?: string; +} +/** + * GpuClaimStatus represents runtime state and metadata about allocation progress. + */ +export interface GpuClaimStatus { + phase?: string; + message?: string; + updatedAt?: string; + lastSynced?: string; +} + ////////// // source: gpu_group.go @@ -754,11 +876,13 @@ export interface Quota { diskSize: number /* float64 */; snapshots: number /* int */; gpuLeaseDuration: number /* float64 */; // in hours + gpus: number /* int */; } export interface Usage { cpuCores: number /* float64 */; ram: number /* float64 */; diskSize: number /* int */; + gpus: number /* int */; } ////////// diff --git a/export/types/v2/query/index.ts b/export/types/v2/query/index.ts index bf860ee9..3bcc1f79 100644 --- a/export/types/v2/query/index.ts +++ b/export/types/v2/query/index.ts @@ -35,6 +35,14 @@ export interface DeploymentUpdate { envs: { [key: string]: string}[]; } +////////// +// source: gpu_claim.go + +export interface GpuClaimList { + Pagination?: Pagination; + Detailed: boolean; +} + ////////// // source: gpu_group.go diff --git a/export/types/v2/uri/index.ts b/export/types/v2/uri/index.ts index 1072a3a6..4d4ee0a7 100644 --- a/export/types/v2/uri/index.ts +++ b/export/types/v2/uri/index.ts @@ -25,6 +25,18 @@ export interface BuildGet { DeploymentID: string; } +////////// +// source: gpu_claim.go + +export interface GpuClaimList { +} +export interface GpuClaimGet { + GpuClaimID: string; +} +export interface GpuClaimDelete { + GpuClaimID: string; +} + ////////// // source: gpu_group.go diff --git a/go.mod b/go.mod index b241cbfe..2fdfcc25 100644 --- a/go.mod +++ b/go.mod @@ -1,133 +1,148 @@ module github.com/kthcloud/go-deploy -go 1.22.0 - -toolchain go1.22.4 +go 1.25.2 require ( github.com/fatih/structs v1.1.0 - github.com/gin-contrib/cors v1.7.2 - github.com/gin-contrib/zap v1.1.3 - github.com/gin-gonic/gin v1.10.0 - github.com/go-openapi/errors v0.22.0 - github.com/go-openapi/runtime v0.28.0 - github.com/go-openapi/strfmt v0.23.0 - github.com/go-openapi/swag v0.23.0 - github.com/go-openapi/validate v0.24.0 - github.com/go-playground/validator/v10 v10.22.0 - github.com/golang/glog v1.2.2 - github.com/google/go-cmp v0.6.0 + github.com/gin-contrib/cors v1.7.6 + github.com/gin-contrib/zap v1.1.5 + github.com/gin-gonic/gin v1.11.0 + github.com/go-openapi/errors v0.22.3 + github.com/go-openapi/runtime v0.29.0 + github.com/go-openapi/strfmt v0.24.0 + github.com/go-openapi/swag v0.25.1 + github.com/go-openapi/validate v0.25.0 + github.com/go-playground/validator/v10 v10.28.0 + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/golang/glog v1.2.5 + github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/helloyi/go-sshclient v1.2.0 github.com/mitchellh/mapstructure v1.5.0 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/penglongli/gin-metrics v0.1.10 + github.com/penglongli/gin-metrics v0.1.13 github.com/rancher/go-rancher v0.1.0 - github.com/redis/go-redis/v9 v9.6.1 - github.com/stretchr/testify v1.9.0 + github.com/redis/go-redis/v9 v9.14.1 + github.com/stretchr/testify v1.11.1 github.com/swaggo/files/v2 v2.0.2 github.com/swaggo/swag/v2 v2.0.0-rc4 - go.mongodb.org/mongo-driver v1.17.2 + go.mongodb.org/mongo-driver v1.17.4 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.32.0 - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - golang.org/x/net v0.27.0 - golang.org/x/oauth2 v0.21.0 - gopkg.in/square/go-jose.v2 v2.6.0 + golang.org/x/crypto v0.43.0 + golang.org/x/exp v0.0.0-20251017212417-90e834f514db + golang.org/x/net v0.46.0 + golang.org/x/oauth2 v0.32.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.30.3 - k8s.io/apimachinery v0.30.3 - k8s.io/client-go v0.30.3 - kubevirt.io/api v1.3.0 - kubevirt.io/containerized-data-importer-api v1.59.0 + k8s.io/api v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + kubevirt.io/api v1.6.2 + kubevirt.io/containerized-data-importer-api v1.63.1 ) +require github.com/sv-tools/openapi v0.2.1 // indirect + require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.13.0 // indirect - github.com/bytedance/sonic v1.11.9 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/bits-and-blooms/bitset v1.24.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.1 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/emicklei/go-restful/v3 v3.12.1 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/gabriel-vasile/mimetype v1.4.5 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/analysis v0.24.0 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/loads v0.23.1 // indirect + github.com/go-openapi/spec v0.22.0 // indirect + github.com/go-openapi/swag/cmdutils v0.25.1 // indirect + github.com/go-openapi/swag/conv v0.25.1 // indirect + github.com/go-openapi/swag/fileutils v0.25.1 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/swag/jsonutils v0.25.1 // indirect + github.com/go-openapi/swag/loading v0.25.1 // indirect + github.com/go-openapi/swag/mangling v0.25.1 // indirect + github.com/go-openapi/swag/netutils v0.25.1 // indirect + github.com/go-openapi/swag/stringutils v0.25.1 // indirect + github.com/go-openapi/swag/typeutils v0.25.1 // indirect + github.com/go-openapi/swag/yamlutils v0.25.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/goccy/go-json v0.10.3 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/gofuzz v1.2.0 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/imdario/mergo v0.3.16 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.9 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/fs v0.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/ulid v1.3.1 // indirect - github.com/openshift/api v0.0.0-20240724184751-84047ef4a2ce // indirect github.com/openshift/custom-resource-status v1.1.2 // indirect - github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/sftp v1.13.5 // indirect + github.com/pkg/sftp v1.13.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/sv-tools/openapi v0.2.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.23.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.30.3 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240726031636-6f6746feab9c // indirect - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect kubevirt.io/controller-lifecycle-operator-sdk/api v0.2.4 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 7eaa0603..fb174a30 100644 --- a/go.sum +++ b/go.sum @@ -1,88 +1,45 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= -github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.1 h1:hqnfFbjjk3pxGa5E9Ho3hjoU7odtUuNmJ9Ao+Bo8s1c= +github.com/bits-and-blooms/bitset v1.24.1/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg= -github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= +github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= @@ -90,120 +47,119 @@ github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkg github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.15.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= -github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= -github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-contrib/zap v1.1.3 h1:9e/U9fYd4/OBfmSEBs5hHZq114uACn7bpuzvCkcJySA= -github.com/gin-contrib/zap v1.1.3/go.mod h1:+BD/6NYZKJyUpqVoJEvgeq9GLz8pINEQvak9LHNOTSE= -github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-contrib/zap v1.1.5 h1:qKwhWb4DQgPriCl1AHLLob6hav/KUIctKXIjTmWIN3I= +github.com/gin-contrib/zap v1.1.5/go.mod h1:lAchUtGz9M2K6xDr1rwtczyDrThmSx6c9F384T45iOE= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA= +github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw= +github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs= +github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= -github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/loads v0.23.1 h1:H8A0dX2KDHxDzc797h0+uiCZ5kwE2+VojaQVaTlXvS0= +github.com/go-openapi/loads v0.23.1/go.mod h1:hZSXkyACCWzWPQqizAv/Ye0yhi2zzHwMmoXQ6YQml44= +github.com/go-openapi/runtime v0.29.0 h1:Y7iDTFarS9XaFQ+fA+lBLngMwH6nYfqig1G+pHxMRO0= +github.com/go-openapi/runtime v0.29.0/go.mod h1:52HOkEmLL/fE4Pg3Kf9nxc9fYQn0UsIWyGjGIJE9dkg= +github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw= +github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0= +github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc= +github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8= +github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo= +github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY= +github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= +github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= +github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU= +github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= +github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= +github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= +github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= +github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= +github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= +github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4= +github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE= +github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= +github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= +github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= +github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= +github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= +github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= +github.com/go-openapi/validate v0.25.0 h1:JD9eGX81hDTjoY3WOzh6WqxVBVl7xjsLnvDo1GL5WPU= +github.com/go-openapi/validate v0.25.0/go.mod h1:SUY7vKrN5FiwK6LyvSwKjDfLNirSfWwHNgxd2l29Mmw= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= -github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= -github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -211,51 +167,28 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= @@ -265,41 +198,22 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/helloyi/go-sshclient v1.2.0 h1:36YOcHjtb3QhtZPTFthb0kvDlfQqVHErwfObVq6omck= github.com/helloyi/go-sshclient v1.2.0/go.mod h1:L2+lPFL4TshqEu5fl5FHqtojNDzUtPFIjHXgaZYMX0Q= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -308,18 +222,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -327,17 +239,15 @@ github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0Gq github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -351,92 +261,68 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= -github.com/openshift/api v0.0.0-20240724184751-84047ef4a2ce h1:AR9XMlwc7akIN13KDx4L0tI04zHf8jEZ1z1RMRbz1J0= -github.com/openshift/api v0.0.0-20240724184751-84047ef4a2ce/go.mod h1:OOh6Qopf21pSzqNVCB5gomomBXb8o5sGKZxG2KNpaXM= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/openshift/custom-resource-status v1.1.2 h1:C3DL44LEbvlbItfd8mT5jWrqPfHnSOQoQf/sypqA6A4= github.com/openshift/custom-resource-status v1.1.2/go.mod h1:DB/Mf2oTeiAmVVX1gN+NEqweonAPY0TKUwADizj8+ZA= -github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/penglongli/gin-metrics v0.1.10 h1:mNNWCM3swMOVHwzrHeXsE4C/myu8P/HIFohtyMi9rN8= -github.com/penglongli/gin-metrics v0.1.10/go.mod h1:wxGsGUwpVGv3hmYSxQn2GZgRL3YuCgiRFq2d0X6+EOU= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/penglongli/gin-metrics v0.1.13 h1:a1wyrXcbUVxL5w4c2TSv+9kyQA9qM1o23h0V6SdSHgQ= +github.com/penglongli/gin-metrics v0.1.13/go.mod h1:VEmSyx/9TwUG50IsPCgjMKOUuGO74V2lmkLZ6x1Dlko= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= +github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= +github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/rancher/go-rancher v0.1.0 h1:YIKWwe5giu2WICfyCcGqX+m4XTRbMpA8vzLxl1Kwb7w= github.com/rancher/go-rancher v0.1.0/go.mod h1:7oQvGNiJsGvrUgB+7AH8bmdzuR0uhULfwKb43Ht0hUk= -github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/redis/go-redis/v9 v9.14.1 h1:nDCrEiJmfOWhD76xlaw+HXT0c9hfNWeXgl0vIRYSDvQ= +github.com/redis/go-redis/v9 v9.14.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA= github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= @@ -445,10 +331,10 @@ github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77Mrv github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -457,123 +343,82 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= -go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= +go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw= +golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -581,82 +426,52 @@ golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -664,147 +479,74 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -814,30 +556,26 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -848,22 +586,17 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ= -k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= -k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= -k8s.io/apiextensions-apiserver v0.30.3 h1:oChu5li2vsZHx2IvnGP3ah8Nj3KyqG3kRSaKmijhB9U= -k8s.io/apiextensions-apiserver v0.30.3/go.mod h1:uhXxYDkMAvl6CJw4lrDN4CPbONkF3+XL9cacCT44kV4= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= k8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc= -k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k= -k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= k8s.io/code-generator v0.23.3/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -875,31 +608,28 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= -k8s.io/kube-openapi v0.0.0-20240726031636-6f6746feab9c h1:CHL3IcTrTI3csK36iwYJy36uQRic+IpSoRMNH+0I8SE= -k8s.io/kube-openapi v0.0.0-20240726031636-6f6746feab9c/go.mod h1:0CVn9SVo8PeW5/JgsBZZIFmmTk5noOM8WXf2e1tCihE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -kubevirt.io/api v1.3.0 h1:9sGElMmnRU50pGED+MPPD2OwQl4S5lvjCUjm+t0mI90= -kubevirt.io/api v1.3.0/go.mod h1:e6LkElYZZm8NcP2gKlFVHZS9pgNhIARHIjSBSfeiP1s= -kubevirt.io/containerized-data-importer-api v1.59.0 h1:GdDt9BlR0qHejpMaPfASbsG8JWDmBf1s7xZBj5W9qn0= -kubevirt.io/containerized-data-importer-api v1.59.0/go.mod h1:4yOGtCE7HvgKp7wftZZ3TBvDJ0x9d6N6KaRjRYcUFpE= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +kubevirt.io/api v1.6.2 h1:aoqZ4KsbOyDjLnuDw7H9wEgE/YTd/q5BBmYeQjJNizc= +kubevirt.io/api v1.6.2/go.mod h1:p66fEy/g79x7VpgUwrkUgOoG2lYs5LQq37WM6JXMwj4= +kubevirt.io/containerized-data-importer-api v1.63.1 h1:g2I9za0QEscRsQjOOK/MM0feywp1x9Gl8IyT6Egtg0g= +kubevirt.io/containerized-data-importer-api v1.63.1/go.mod h1:VGp35wxpLXU18b7cnEpmcThI3AjcZUSfg/Zfql44U4o= kubevirt.io/controller-lifecycle-operator-sdk/api v0.2.4 h1:fZYvD3/Vnitfkx6IJxjLAk8ugnZQ7CXVYcRfkSKmuZY= kubevirt.io/controller-lifecycle-operator-sdk/api v0.2.4/go.mod h1:018lASpFYBsYN6XwmA2TIrPCx6e0gviTd/ZNtSitKgc= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/main.go b/main.go index b65b7676..c5c43542 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,10 @@ package main import ( + "context" "os" + "os/signal" + "syscall" "github.com/kthcloud/go-deploy/cmd" ) @@ -28,6 +31,9 @@ import ( // @Name Authorization func main() { options := cmd.ParseFlags() + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + options.Ctx = ctx deployApp := cmd.Create(options) if deployApp == nil { @@ -35,6 +41,5 @@ func main() { } defer deployApp.Stop() - quit := make(chan os.Signal) - <-quit + <-ctx.Done() } diff --git a/models/config/config.go b/models/config/config.go index 41f45c87..6d292fc5 100644 --- a/models/config/config.go +++ b/models/config/config.go @@ -1,10 +1,11 @@ package config import ( + "time" + "github.com/kthcloud/go-deploy/models/model" "github.com/kthcloud/go-deploy/pkg/imp/kubevirt/kubevirt" "k8s.io/client-go/kubernetes" - "time" ) // The following structs are used to parse the config.yaml file @@ -13,6 +14,7 @@ import ( const ( ZoneCapabilityDeployment = "deployment" ZoneCapabilityVM = "vm" + ZoneCapabilityDRA = "dra" ) type ConfigType struct { diff --git a/models/model/deployment_convert.go b/models/model/deployment_convert.go index 669781d7..c94b0070 100644 --- a/models/model/deployment_convert.go +++ b/models/model/deployment_convert.go @@ -142,6 +142,19 @@ func (deployment *Deployment) ToDTO(smURL *string, externalPort *int, teams []st } } + var gpus []body.DeploymentGPU = make([]body.DeploymentGPU, 0, len(app.GPUs)) + for _, gpu := range app.GPUs { + dto := body.DeploymentGPU{ + Name: gpu.Name, + ClaimName: gpu.ClaimName, + } + if gpu.ClaimName == "" { + continue + } + + gpus = append(gpus, dto) + } + return body.DeploymentRead{ ID: deployment.ID, Name: deployment.Name, @@ -160,6 +173,7 @@ func (deployment *Deployment) ToDTO(smURL *string, externalPort *int, teams []st CpuCores: app.CpuCores, RAM: app.RAM, Replicas: app.Replicas, + GPUs: gpus, }, Envs: envs, @@ -250,6 +264,18 @@ func (p *DeploymentCreateParams) FromDTO(dto *body.DeploymentCreate, fallbackZon p.InitCommands = dto.InitCommands p.Args = dto.Args + p.GPUs = make([]DeploymentGPU, 0, len(dto.GPUs)) + for _, gpu := range dto.GPUs { + gpuM := DeploymentGPU{ + Name: gpu.Name, + ClaimName: gpu.ClaimName, + } + if gpu.ClaimName == "" { + continue + } + p.GPUs = append(p.GPUs, gpuM) + } + if dto.HealthCheckPath == nil { p.PingPath = "/healthz" } else { @@ -328,6 +354,23 @@ func (p *DeploymentUpdateParams) FromDTO(dto *body.DeploymentUpdate, deploymentT p.Volumes = &volumes } + if dto.GPUs != nil { + gpus := make([]DeploymentGPU, 0, len(*dto.GPUs)) + for _, gpu := range *dto.GPUs { + var gpuM DeploymentGPU = DeploymentGPU{ + Name: gpu.Name, + ClaimName: gpu.ClaimName, + } + + if gpu.ClaimName == "" { + continue + } + + gpus = append(gpus, gpuM) + } + p.GPUs = &gpus + } + // Convert custom domain to puny encoded if dto.CustomDomain != nil { if punyEncoded, err := idna.New().ToASCII(*dto.CustomDomain); err == nil { diff --git a/models/model/deployment_params.go b/models/model/deployment_params.go index 7edaa145..99ac9a18 100644 --- a/models/model/deployment_params.go +++ b/models/model/deployment_params.go @@ -7,6 +7,7 @@ type DeploymentCreateParams struct { CpuCores float64 RAM float64 Replicas int + GPUs []DeploymentGPU Image string InternalPort int @@ -30,6 +31,7 @@ type DeploymentUpdateParams struct { CpuCores *float64 RAM *float64 + GPUs *[]DeploymentGPU Envs *[]DeploymentEnv InternalPort *int diff --git a/models/model/deployment_related.go b/models/model/deployment_related.go index a46d0302..011a814c 100644 --- a/models/model/deployment_related.go +++ b/models/model/deployment_related.go @@ -25,16 +25,15 @@ const ( VisibilityAuth = "auth" ) -var ( - EmptyReplicaStatus = &ReplicaStatus{} -) +var EmptyReplicaStatus = &ReplicaStatus{} type App struct { Name string `bson:"name"` - CpuCores float64 `bson:"cpuCores,omitempty"` - RAM float64 `bson:"ram,omitempty"` - Replicas int `bson:"replicas"` + CpuCores float64 `bson:"cpuCores,omitempty"` + RAM float64 `bson:"ram,omitempty"` + Replicas int `bson:"replicas"` + GPUs []DeploymentGPU `bson:"gpus,omitempty"` Image string `bson:"image"` InternalPort int `bson:"internalPort"` @@ -94,9 +93,15 @@ type DeploymentVolume struct { ServerPath string `bson:"serverPath"` } +type DeploymentGPU struct { + Name string `bson:"name"` + ClaimName string `bson:"claimName"` +} + type DeploymentUsage struct { CpuCores float64 RAM float64 + Gpus int } type DeploymentError struct { diff --git a/models/model/gpu_claim.go b/models/model/gpu_claim.go new file mode 100644 index 00000000..68fecd51 --- /dev/null +++ b/models/model/gpu_claim.go @@ -0,0 +1,344 @@ +package model + +import ( + "errors" + "fmt" + "slices" + "strings" + "time" + + "github.com/kthcloud/go-deploy/dto/v2/body" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra/nvidia" + "github.com/kthcloud/go-deploy/utils" + "go.mongodb.org/mongo-driver/bson" +) + +var ( + ErrUnknownParameterImplType = errors.New("unknown underlying opaque GPU config type") + ErrCouldNotInferDriver = errors.New("could not infer driver") +) + +// GpuClaim represents a DRA-style claim for one or more GPUs +// that can be requested and consumed by deployments or workloads. +type GpuClaim struct { + ID string `bson:"id"` + Name string `bson:"name"` + Zone string `bson:"zone"` + + // Requested contains all requested GPU configurations by key request.Name + // this key is the key that users will request for + Requested map[string]RequestedGpu `bson:"requested"` + + // Allocated contains the GPUs that have been successfully bound/allocated + Allocated map[string][]AllocatedGpu `bson:"allocated,omitempty"` + + // Consumers are the workloads currently using this claim + Consumers []GpuClaimConsumer `bson:"consumers,omitempty"` + + // Status reflects the reconciliation and/or lifecycle state + Status *GpuClaimStatus `bson:"status,omitempty"` + + // LastError holds the last reconciliation or provisioning error + LastError error `bson:"lastError,omitempty"` + + // The roles that are allowed to use this GpuClaim + // Empty means all roles + AllowedRoles []string `bson:"allowedRoles,omitempty"` + + Activities map[string]Activity `bson:"activities"` + + Subsystems GpuClaimSubsystems `bson:"subsystems"` + + CreatedAt time.Time `bson:"createdAt"` + UpdatedAt *time.Time `bson:"updatedAt,omitempty"` +} + +// RequestAllocationMode defines how GPUs should be allocated. +type RequestAllocationMode string + +const ( + RequestAllocationMode_None RequestAllocationMode = "" + RequestAllocationMode_All RequestAllocationMode = "All" + RequestAllocationMode_ExactCount RequestAllocationMode = "ExactCount" +) + +type RequestedGpuCreate struct { + RequestedGpu `mapstructure:",squash" json:",inline" bson:",inline"` + Name string `json:"name" bson:"name"` +} + +// RequestedGpu describes the desired GPU configuration a workload requests. +type RequestedGpu struct { + AllocationMode RequestAllocationMode `bson:"allocationMode"` + Capacity map[string]string `bson:"capacity,omitempty"` // e.g. memory: "16Gi" + Count *int64 `bson:"count,omitempty"` + DeviceClassName string `bson:"deviceClassName"` + Selectors []string `bson:"selectors,omitempty"` + Config *GpuDeviceConfiguration `bson:"config,omitempty"` +} + +// GpuDeviceConfiguration holds the DRA opaque driver parameters and metadata. +type GpuDeviceConfiguration struct { + Driver string `bson:"driver"` + Parameters dra.OpaqueParams `bson:"parameters,omitempty"` +} + +func (cfg GpuDeviceConfiguration) MarshalBSON() ([]byte, error) { + doc := bson.M{ + "driver": cfg.Driver, + } + + if cfg.Parameters != nil { + // Marshal interface to BSON using its JSON form (to preserve structure) + paramBytes, err := bson.Marshal(cfg.Parameters) + if err != nil { + return nil, fmt.Errorf("failed to marshal parameters: %w", err) + } + + var paramDoc bson.M + if err := bson.Unmarshal(paramBytes, ¶mDoc); err != nil { + return nil, err + } + doc["parameters"] = paramDoc + } + + return bson.Marshal(doc) +} + +func (cfg *GpuDeviceConfiguration) UnmarshalBSON(data []byte) error { + var raw bson.M + if err := bson.Unmarshal(data, &raw); err != nil { + return fmt.Errorf("failed to unmarshal raw data: %w", err) + } + + // Extract driver first + if d, ok := raw["driver"].(string); ok { + cfg.Driver = d + } + + // Nothing more to do if no parameters + paramsRaw, ok := raw["parameters"] + if !ok || paramsRaw == nil { + return nil + } + + // Marshal the inner parameters object for re-decoding + paramBytes, err := bson.Marshal(paramsRaw) + if err != nil { + return fmt.Errorf("failed to re-marshal parameters: %w", err) + } + + switch cfg.Driver { + case "gpu.nvidia.com": + var nvidia nvidia.GPUConfigParametersImpl + if err := bson.Unmarshal(paramBytes, &nvidia); err != nil { + return fmt.Errorf("failed to decode nvidia parameters: %w", err) + } + cfg.Parameters = nvidia + + default: + var generic dra.OpaqueParams + if err := bson.Unmarshal(paramBytes, &generic); err != nil { + return fmt.Errorf("failed to decode generic parameters: %w", err) + } + cfg.Parameters = generic + } + + return nil +} + +// InferDriver attempts to infer the GPU driver based on the OpaqueParams implementation. +func (g GpuDeviceConfiguration) InferDriver() (string, error) { + if g.Parameters != nil { + switch g.Parameters.(type) { + case nvidia.GPUConfigParametersImpl: + return "gpu.nvidia.com", nil + // If some time in the future we have + // more impls we can add support for + // other vendors here: + // case amd.GPUConfigParametersImpl: + // return "gpu.amd.com", nil + default: + return "", ErrUnknownParameterImplType + } + } + return "", ErrCouldNotInferDriver +} + +// AllocatedGpu represents a concrete allocated GPU or GPU share. +type AllocatedGpu struct { + Pool string `bson:"pool,omitempty"` + Device string `bson:"device,omitempty"` + ShareID string `bson:"shareID,omitempty"` + AdminAccess bool `bson:"adminAccess,omitempty"` +} + +// GpuClaimConsumer describes a workload (Pod/Deployment/etc.) consuming this GPU claim. +type GpuClaimConsumer struct { + APIGroup string `bson:"apiGroup,omitempty"` + Resource string `bson:"resource,omitempty"` + Name string `bson:"name,omitempty"` + UID string `bson:"uid,omitempty"` +} + +type GpuClaimStatusPhase string + +const ( + GpuClaimStatusPhase_Unknown GpuClaimStatusPhase = "" + GpuClaimStatusPhase_Pending GpuClaimStatusPhase = "pending" + GpuClaimStatusPhase_Bound GpuClaimStatusPhase = "bound" + GpuClaimStatusPhase_Failed GpuClaimStatusPhase = "failed" +) + +// GpuClaimStatus represents runtime state and metadata about allocation progress. +type GpuClaimStatus struct { + Phase GpuClaimStatusPhase `bson:"phase,omitempty"` // e.g. pending, bound, released, failed + Message string `bson:"message,omitempty"` + UpdatedAt *time.Time `bson:"updatedAt,omitempty"` + LastSynced *time.Time `bson:"lastSynced,omitempty"` +} + +func (g GpuClaim) ToDTO() body.GpuClaimRead { + dto := body.GpuClaimRead{ + ID: g.ID, + Name: g.Name, + Zone: g.Zone, + AllowedRoles: slices.Clone(g.AllowedRoles), + CreatedAt: g.CreatedAt, + UpdatedAt: g.UpdatedAt, + LastError: utils.ErrorStr(g.LastError), + } + + // Convert Requested + dto.Requested = make(map[string]body.RequestedGpu) + for key, req := range g.Requested { + dto.Requested[key] = body.RequestedGpu{ + AllocationMode: string(req.AllocationMode), + Capacity: req.Capacity, + Count: req.Count, + DeviceClassName: req.DeviceClassName, + Selectors: req.Selectors, + Config: func() *body.GpuDeviceConfigurationWrapper { + if req.Config == nil { + return nil + } + if req.Config.Parameters == nil { + driver := strings.TrimSpace(req.Config.Driver) + if driver == "" { + return nil + } + switch driver { + case "gpu.nvidia.com": + return &body.GpuDeviceConfigurationWrapper{ + GpuDeviceConfiguration: body.NvidiaDeviceConfiguration{ + Driver: driver, + }, + } + default: + return &body.GpuDeviceConfigurationWrapper{ + GpuDeviceConfiguration: body.GenericDeviceConfiguration{ + Driver: driver, + }, + } + } + } + switch t := req.Config.Parameters.(type) { + case nvidia.GPUConfigParametersImpl: + return &body.GpuDeviceConfigurationWrapper{ + GpuDeviceConfiguration: body.NvidiaDeviceConfiguration{ + Driver: req.Config.Driver, + Parameters: &t.GpuConfig, + }, + } + default: + return &body.GpuDeviceConfigurationWrapper{ + GpuDeviceConfiguration: body.GenericDeviceConfiguration{ + Driver: req.Config.Driver, + }, + } + } + }(), + } + } + + // Convert Allocated + dto.Allocated = make(map[string][]body.AllocatedGpu) + for key, allocs := range g.Allocated { + dto.Allocated[key] = make([]body.AllocatedGpu, len(allocs)) + for i, alloc := range allocs { + dto.Allocated[key][i] = body.AllocatedGpu{ + Pool: alloc.Pool, + Device: alloc.Device, + ShareID: alloc.ShareID, + AdminAccess: alloc.AdminAccess, + } + } + + } + + // Convert Consumers + for _, c := range g.Consumers { + dto.Consumers = append(dto.Consumers, body.GpuClaimConsumer{ + APIGroup: c.APIGroup, + Resource: c.Resource, + Name: c.Name, + UID: c.UID, + }) + } + + // Convert Status + if g.Status != nil { + dto.Status = &body.GpuClaimStatus{ + Phase: string(g.Status.Phase), + Message: g.Status.Message, + UpdatedAt: g.Status.UpdatedAt, + LastSynced: g.Status.LastSynced, + } + } + + return dto +} + +func (g GpuClaim) ToBriefDTO() body.GpuClaimRead { + req := make(map[string]body.RequestedGpu, len(g.Requested)) + for k, v := range g.Requested { + req[k] = body.RequestedGpu{ + AllocationMode: string(v.AllocationMode), + DeviceClassName: v.DeviceClassName, + } + } + dto := body.GpuClaimRead{ + ID: g.ID, + Name: g.Name, + Zone: g.Zone, + CreatedAt: g.CreatedAt, + UpdatedAt: g.UpdatedAt, + Requested: req, + } + + return dto +} + +// DoingActivity returns true if the gpuClaim is doing the given activity. +func (gc *GpuClaim) DoingActivity(activity string) bool { + for _, a := range gc.Activities { + if a.Name == activity { + return true + } + } + return false +} + +// Check if a user is allowed to use a gpuClaim +func (gc *GpuClaim) HasAccess(roles ...string) bool { + if len(gc.AllowedRoles) == 0 { + return true + } + for _, role := range roles { + if slices.Contains(gc.AllowedRoles, role) { + return true + } + } + return false +} diff --git a/models/model/gpu_claim_params.go b/models/model/gpu_claim_params.go new file mode 100644 index 00000000..e19200ac --- /dev/null +++ b/models/model/gpu_claim_params.go @@ -0,0 +1,19 @@ +package model + +type GpuClaimCreateParams struct { + Name string `json:"name" bson:"name"` + Zone string `json:"zone" bson:"zone"` + + Requested []RequestedGpuCreate `json:"requested" bson:"requested"` + + AllowedRoles []string `bson:"allowedRoles,omitempty"` +} + +type GpuClaimUpdateParams struct { + Name *string `json:"name" bson:"name"` + Zone *string `json:"zone" bson:"zone"` + + Requested *[]RequestedGpuCreate `json:"requested" bson:"requested"` + + AllowedRoles *[]string `bson:"allowedRoles,omitempty"` +} diff --git a/models/model/gpu_claim_subsystems.go b/models/model/gpu_claim_subsystems.go new file mode 100644 index 00000000..43e4b974 --- /dev/null +++ b/models/model/gpu_claim_subsystems.go @@ -0,0 +1,18 @@ +package model + +import k8sModels "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/models" + +type GpuClaimSubsystems struct { + K8s GpuClaimK8s `bson:"k8s"` +} + +type GpuClaimK8s struct { + Namespace k8sModels.NamespacePublic `bson:"namespace"` + + ResourceClaimMap map[string]k8sModels.ResourceClaimPublic `bson:"resourceClaimMap,omitempty"` +} + +// GetNamespace returns the namespace of the resourceClaim. +func (k *GpuClaimK8s) GetNamespace() *k8sModels.NamespacePublic { + return &k.Namespace +} diff --git a/models/model/job.go b/models/model/job.go index 07cfc4cb..1bbf5cd4 100644 --- a/models/model/job.go +++ b/models/model/job.go @@ -47,6 +47,13 @@ const ( JobDeleteSM = "deleteSm" // JobRepairSM is used when repairing a storage manager. JobRepairSM = "repairSm" + + // JobCreateGpuClaim is used when creating a gpu claim. + JobCreateGpuClaim = "createGpuClaim" + // JobCreateGpuClaim is used when deleting a gpu claim. + JobDeleteGpuClaim = "deleteGpuClaim" + // JobUpdateGpuClaim is used when updating a gpu claim + JobUpdateGpuClaim = "updateGpuClaim" ) const ( diff --git a/models/model/permissions.go b/models/model/permissions.go index a58e0d09..49c14908 100644 --- a/models/model/permissions.go +++ b/models/model/permissions.go @@ -6,4 +6,7 @@ type Permissions struct { UseCustomDomains bool `yaml:"useCustomDomains" structs:"useCustomDomains"` UseGPUs bool `yaml:"useGpus" structs:"useGpus"` UsePrivilegedGPUs bool `yaml:"usePrivilegedGpus" structs:"usePrivilegedGpus"` + // If non nil then use explicit value, if nil then we allow use of vms (for backward compatibilty). + // Only checked when creating a new VM, if a user already has a VM then we allow them to update it. + UseVms *bool `yaml:"useVms,omitempty" structs:"useVms,omitempty"` } diff --git a/models/model/quotas.go b/models/model/quotas.go index 7bd8d345..6c03d8be 100644 --- a/models/model/quotas.go +++ b/models/model/quotas.go @@ -10,6 +10,7 @@ type Quotas struct { DiskSize float64 `yaml:"diskSize" structs:"diskSize"` Snapshots int `yaml:"snapshots" structs:"snapshots"` GpuLeaseDuration float64 `yaml:"gpuLeaseDuration" structs:"gpuLeaseDuration"` // in hours + Gpus int `yaml:"gpus" structs:"gpus"` } // ToDTO converts a Quotas to a body.Quota DTO. @@ -20,5 +21,6 @@ func (q *Quotas) ToDTO() body.Quota { DiskSize: q.DiskSize, Snapshots: q.Snapshots, GpuLeaseDuration: q.GpuLeaseDuration, + Gpus: q.Gpus, } } diff --git a/models/model/role.go b/models/model/role.go index 944ee9e3..ae469d2e 100644 --- a/models/model/role.go +++ b/models/model/role.go @@ -1,9 +1,10 @@ package model import ( + "sort" + "github.com/fatih/structs" body2 "github.com/kthcloud/go-deploy/dto/v2/body" - "sort" ) type Role struct { @@ -25,6 +26,11 @@ func (r *Role) ToDTO(includeQuota bool) body2.Role { } } + // If we dont explicitly disallow it they have the perm + if r.Permissions.UseVms == nil { + permissions = append(permissions, "useVms") + } + var quota *body2.Quota if includeQuota { dto := r.Quotas.ToDTO() diff --git a/models/model/user.go b/models/model/user.go index 80757669..c49ae162 100644 --- a/models/model/user.go +++ b/models/model/user.go @@ -4,12 +4,10 @@ import ( "time" ) -var ( - // FetchGravatarInterval is the interval at which we fetch the gravatar - // image for a user. This is done to prevent fetching the image on every - // request. - FetchGravatarInterval = 10 * time.Minute -) +// FetchGravatarInterval is the interval at which we fetch the gravatar +// image for a user. This is done to prevent fetching the image on every +// request. +var FetchGravatarInterval = 10 * time.Minute const ( TestAdminUserID = "955f0f87-37fd-4792-90eb-9bf6989e698a" @@ -64,6 +62,7 @@ type UserUsage struct { RAM float64 `bson:"ram"` DiskSize int `bson:"diskSize"` Snapshots int `bson:"snapshots"` + Gpus int `bson:"gpus"` } type EffectiveRole struct { diff --git a/models/model/user_convert.go b/models/model/user_convert.go index 0cd5ae4f..d92405ed 100644 --- a/models/model/user_convert.go +++ b/models/model/user_convert.go @@ -1,9 +1,10 @@ package model import ( + "math" + "github.com/kthcloud/go-deploy/dto/v2/body" "github.com/kthcloud/go-deploy/pkg/log" - "math" ) // ToDTO converts a User to a body.UserRead DTO. @@ -75,6 +76,7 @@ func (usage *UserUsage) ToDTO() body.Usage { CpuCores: math.Round(usage.CpuCores*10) / 10, RAM: math.Round(usage.RAM*10) / 10, DiskSize: usage.DiskSize, + Gpus: usage.Gpus, } } diff --git a/models/version/versions.go b/models/version/versions.go index b5a3e671..9ab4afdb 100644 --- a/models/version/versions.go +++ b/models/version/versions.go @@ -1,7 +1,7 @@ package version const ( - AppVersion = "1.0.3" + AppVersion = "1.1.0" V2 = "v2" ) diff --git a/pkg/auth/gin_keycloak.go b/pkg/auth/gin_keycloak.go index 3f769f48..a432c86b 100644 --- a/pkg/auth/gin_keycloak.go +++ b/pkg/auth/gin_keycloak.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "fmt" "io" "math/big" "net/http" @@ -19,10 +20,10 @@ import ( "github.com/kthcloud/go-deploy/pkg/config" "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" "github.com/golang/glog" "github.com/patrickmn/go-cache" "golang.org/x/oauth2" - "gopkg.in/square/go-jose.v2/jwt" ) // VarianceTimer controls the max runtime of SetupKeycloakChain() and AuthChain() middleware @@ -153,25 +154,47 @@ func getPublicKeyFromCacheOrBackend(keyId string, config KeycloakConfig) (KeyEnt } func decodeToken(token *oauth2.Token, config KeycloakConfig) (*KeycloakToken, error) { - keyCloakToken := KeycloakToken{} - var err error - parsedJWT, err := jwt.ParseSigned(token.AccessToken) + var keycloakToken KeycloakToken + + // Parse the token and extract the kid + parsed, err := jwt.Parse(token.AccessToken, func(t *jwt.Token) (any, error) { + // Ensure the signing method is as expected (RSA) + if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) + } + + // Extract Key ID (kid) and fetch the appropriate public key + kid, _ := t.Header["kid"].(string) + key, err := getPublicKey(kid, config) + if err != nil { + return nil, fmt.Errorf("failed to get public key: %w", err) + } + return key, nil + }) if err != nil { - glog.Errorf("[Gin-OAuth] jwt not decodable: %s", err) + glog.Errorf("[Gin-OAuth] jwt not decodable: %v", err) return nil, err } - key, err := getPublicKey(parsedJWT.Headers[0].KeyID, config) - if err != nil { - glog.Errorf("Failed to get publickey %v", err) - return nil, err + + // Validate token and extract claims + if claims, ok := parsed.Claims.(jwt.MapClaims); ok && parsed.Valid { + if err := mapToStruct(claims, &keycloakToken); err != nil { + glog.Errorf("Failed to parse claims into KeycloakToken: %v", err) + return nil, err + } + return &keycloakToken, nil } - err = parsedJWT.Claims(key, &keyCloakToken) + glog.Errorf("Invalid JWT or unexpected claims structure") + return nil, fmt.Errorf("invalid JWT or claims") +} + +func mapToStruct(m jwt.MapClaims, v interface{}) error { + data, err := json.Marshal(m) if err != nil { - glog.Errorf("Failed to get claims JWT:%+v", err) - return nil, err + return err } - return &keyCloakToken, nil + return json.Unmarshal(data, v) } func isExpired(token *KeycloakToken) bool { diff --git a/pkg/db/mongo.go b/pkg/db/mongo.go index 89a6dc50..6a051929 100644 --- a/pkg/db/mongo.go +++ b/pkg/db/mongo.go @@ -256,6 +256,11 @@ func GetCollectionDefinitions() map[string]CollectionDefinition { Name: "workerStatus", Indexes: []string{"name", "status"}, }, + "gpuClaims": { + Name: "gpuClaims", + UniqueIndexes: [][]string{{"name"}}, + TotallyUniqueIndexes: [][]string{{"id"}}, + }, } } diff --git a/pkg/db/resources/deployment_repo/client.go b/pkg/db/resources/deployment_repo/client.go index 7932c4c2..7148c257 100644 --- a/pkg/db/resources/deployment_repo/client.go +++ b/pkg/db/resources/deployment_repo/client.go @@ -199,3 +199,44 @@ func (client *Client) Disabled() *Client { return client } + +// WithGpuClaim adds a filter to the client to only include deployments where the provided gpuClaim is referenced. +func (client *Client) WithGpuClaim(claimName string) *Client { + filter := bson.D{ + { + Key: "gpus", + Value: bson.D{ + { + Key: "$elemMatch", + Value: bson.D{ + {Key: "claimName", Value: claimName}, + }, + }, + }, + }, + } + + client.ResourceClient.AddExtraFilter(filter) + return client +} + +// WithGpuClaim adds a filter to the client to only include deployments where the provided gpuClaim and request is referenced. +func (client *Client) WithGpuClaimRequest(claimName string, request string) *Client { + filter := bson.D{ + { + Key: "gpus", + Value: bson.D{ + { + Key: "$elemMatch", + Value: bson.D{ + {Key: "claimName", Value: claimName}, + {Key: "name", Value: request}, + }, + }, + }, + }, + } + + client.ResourceClient.AddExtraFilter(filter) + return client +} diff --git a/pkg/db/resources/deployment_repo/db.go b/pkg/db/resources/deployment_repo/db.go index 0d5c09e5..75410e9b 100644 --- a/pkg/db/resources/deployment_repo/db.go +++ b/pkg/db/resources/deployment_repo/db.go @@ -39,6 +39,7 @@ func (client *Client) Create(id, ownerID string, params *model.DeploymentCreateP CpuCores: params.CpuCores, RAM: params.RAM, Replicas: params.Replicas, + GPUs: params.GPUs, Image: params.Image, InternalPort: params.InternalPort, @@ -144,6 +145,7 @@ func (client *Client) UpdateWithParams(id string, params *model.DeploymentUpdate db.AddIfNotNil(&setUpdate, "apps.main.cpuCores", params.CpuCores) db.AddIfNotNil(&setUpdate, "apps.main.ram", params.RAM) db.AddIfNotNil(&setUpdate, "apps.main.replicas", params.Replicas) + db.AddIfNotNil(&setUpdate, "apps.main.gpus", params.GPUs) db.AddIfNotNil(&setUpdate, "apps.main.visibility", params.Visibility) db.AddIfNotNil(&setUpdate, "neverStale", params.NeverStale) @@ -250,6 +252,7 @@ func (client *Client) GetUsage() (*model.DeploymentUsage, error) { {Key: "apps.main.replicas", Value: 1}, {Key: "apps.main.cpuCores", Value: 1}, {Key: "apps.main.ram", Value: 1}, + {Key: "apps.main.gpus", Value: 1}, } deployments, err := client.ListWithFilterAndProjection(bson.D{}, projection) @@ -263,6 +266,9 @@ func (client *Client) GetUsage() (*model.DeploymentUsage, error) { for _, app := range deployment.Apps { usage.CpuCores += app.CpuCores * float64(app.Replicas) usage.RAM += app.RAM * float64(app.Replicas) + if app.GPUs != nil { + usage.Gpus += len(app.GPUs) * app.Replicas + } } } diff --git a/pkg/db/resources/gpu_claim_repo/client.go b/pkg/db/resources/gpu_claim_repo/client.go new file mode 100644 index 00000000..fb6f9fdd --- /dev/null +++ b/pkg/db/resources/gpu_claim_repo/client.go @@ -0,0 +1,123 @@ +package gpu_claim_repo + +import ( + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/pkg/db" + "github.com/kthcloud/go-deploy/pkg/db/resources/base_clients" + "go.mongodb.org/mongo-driver/bson" +) + +// Client is used to manage GPU claims in the database. +type Client struct { + base_clients.ResourceClient[model.GpuClaim] + base_clients.ActivityResourceClient[model.GpuClaim] +} + +// New returns a new GPU claim client. +func New() *Client { + return &Client{ + ResourceClient: base_clients.ResourceClient[model.GpuClaim]{ + Collection: db.DB.GetCollection("gpuClaims"), + IncludeDeleted: false, + }, + ActivityResourceClient: base_clients.ActivityResourceClient[model.GpuClaim]{ + Collection: db.DB.GetCollection("gpuClaims"), + }, + } +} + +// WithPagination sets the pagination for the client. +func (client *Client) WithPagination(page, pageSize int) *Client { + client.ResourceClient.Pagination = &db.Pagination{ + Page: page, + PageSize: pageSize, + } + + return client +} + +// WithActivities adds a filter to the client to only include storage managers that does +// at least one of the given activities. +func (client *Client) WithActivities(activities ...string) *Client { + orFilter := bson.A{} + + for _, activity := range activities { + orFilter = append(orFilter, bson.M{ + "activities." + activity: bson.M{ + "$exists": true, + }, + }) + } + + filter := bson.D{{ + Key: "$or", Value: orFilter, + }} + + client.ResourceClient.AddExtraFilter(filter) + client.ActivityResourceClient.AddExtraFilter(filter) + + return client +} + +// WithNoActivities adds a filter to the client to only include storage managers without any activities. +func (client *Client) WithNoActivities() *Client { + filter := bson.D{{ + Key: "activities", Value: bson.M{ + "$gte": bson.M{}, + }, + }} + + client.ResourceClient.AddExtraFilter(filter) + client.ActivityResourceClient.AddExtraFilter(filter) + + return client +} + +// WithRoles adds a filter to only get GpuClaims where there are no role requirements +// or the allowedRoles contains any of the provided role values. +func (client *Client) WithRoles(roles []string) *Client { + filter := bson.D{ + { + Key: "$or", + Value: bson.A{ + bson.M{"allowedRoles": bson.M{"$exists": false}}, + bson.M{"allowedRoles": bson.M{"$size": 0}}, + bson.M{"allowedRoles": bson.M{"$in": roles}}, + }, + }, + } + + client.ResourceClient.AddExtraFilter(filter) + + return client +} + +// WithNames adds a filter to match name to one of the provided names +func (client *Client) WithNames(names []string) *Client { + filter := bson.D{ + { + Key: "name", + Value: bson.M{ + "$in": names, + }, + }, + } + + client.ResourceClient.AddExtraFilter(filter) + + return client +} + +// IncludeDeletedResources makes the client include deleted gpu claims. +func (client *Client) IncludeDeletedResources() *Client { + client.ResourceClient.IncludeDeleted = true + + return client +} + +// WithZone adds a filter to the client to only include groups with the given zone. +func (client *Client) WithZone(zone string) *Client { + client.ResourceClient.AddExtraFilter(bson.D{{Key: "zone", Value: zone}}) + + return client +} diff --git a/pkg/db/resources/gpu_claim_repo/db.go b/pkg/db/resources/gpu_claim_repo/db.go new file mode 100644 index 00000000..0aae30f7 --- /dev/null +++ b/pkg/db/resources/gpu_claim_repo/db.go @@ -0,0 +1,92 @@ +package gpu_claim_repo + +import ( + "errors" + "fmt" + "time" + + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/pkg/db" + "github.com/kthcloud/go-deploy/utils/convutils" + "go.mongodb.org/mongo-driver/bson" +) + +func (client *Client) Create(id string, params *model.GpuClaimCreateParams) error { + if params == nil { + return fmt.Errorf("params is nil") + } + + claim := model.GpuClaim{ + ID: id, + Name: params.Name, + Zone: params.Zone, + AllowedRoles: params.AllowedRoles, + Requested: convutils.ToNameMap(params.Requested, func(r model.RequestedGpuCreate) string { return r.Name }, func(r model.RequestedGpuCreate) model.RequestedGpu { + return r.RequestedGpu + }), + Activities: make(map[string]model.Activity), + Subsystems: model.GpuClaimSubsystems{}, + CreatedAt: time.Now(), + } + + // We assume there is a unique constraint on id + zone + name + err := client.CreateIfUnique(id, &claim, bson.D{{Key: "id", Value: id}, {Key: "zone", Value: params.Zone}, {Key: "name", Value: params.Name}}) + if err != nil { + if errors.Is(err, db.ErrUniqueConstraint) { + return ErrGpuClaimAlreadyExists + } + + return err + } + + return nil +} + +type ReconcileOption func(d *bson.D) + +func WithSetStatus(status *model.GpuClaimStatus) ReconcileOption { + return func(d *bson.D) { + (*d) = append(*d, bson.E{Key: "status", Value: status}) + } +} + +func WithSetAllocated(allocated map[string][]model.AllocatedGpu) ReconcileOption { + return func(d *bson.D) { + (*d) = append(*d, bson.E{Key: "allocated", Value: allocated}) + } +} + +func WithSetConsumers(consumers []model.GpuClaimConsumer) ReconcileOption { + return func(d *bson.D) { + (*d) = append(*d, bson.E{Key: "consumers", Value: consumers}) + } +} + +// ReconcileStateByName batches Reconcile options to update the state of a gpu claim. +func (client *Client) ReconcileStateByName(name string, opts ...ReconcileOption) error { + update := bson.D{} + + for _, opt := range opts { + opt(&update) + } + if len(update) < 1 { + // no-op + return nil + } + + return client.SetWithBsonByName(name, update) +} + +// DeleteSubsystem deletes a subsystem from a gpu claim. +// It prepends the key with `subsystems` and unsets it. +func (client *Client) DeleteSubsystem(id, key string) error { + subsystemKey := fmt.Sprintf("subsystems.%s", key) + return client.UpdateWithBsonByID(id, bson.D{{Key: "$unset", Value: bson.D{{Key: subsystemKey, Value: ""}}}}) +} + +// SetSubsystem sets a subsystem in a gpu claim. +// It prepends the key with `subsystems` and sets it. +func (client *Client) SetSubsystem(id, key string, update any) error { + subsystemKey := fmt.Sprintf("subsystems.%s", key) + return client.SetWithBsonByID(id, bson.D{{Key: subsystemKey, Value: update}}) +} diff --git a/pkg/db/resources/gpu_claim_repo/errors.go b/pkg/db/resources/gpu_claim_repo/errors.go new file mode 100644 index 00000000..db0a5370 --- /dev/null +++ b/pkg/db/resources/gpu_claim_repo/errors.go @@ -0,0 +1,8 @@ +package gpu_claim_repo + +import "errors" + +var ( + // ErrGpuClaimAlreadyExists is returned when a GPU Claim already exists with the same name + ErrGpuClaimAlreadyExists = errors.New("gpu claim already exists") +) diff --git a/pkg/jobs/job_definition.go b/pkg/jobs/job_definition.go index 46aa47b3..dcc3bc08 100644 --- a/pkg/jobs/job_definition.go +++ b/pkg/jobs/job_definition.go @@ -4,7 +4,7 @@ import ( "github.com/kthcloud/go-deploy/models/model" "github.com/kthcloud/go-deploy/models/version" "github.com/kthcloud/go-deploy/pkg/jobs/utils" - "github.com/kthcloud/go-deploy/pkg/jobs/v2" + v2 "github.com/kthcloud/go-deploy/pkg/jobs/v2" ) // JobDefinition is a definition of a job. @@ -135,6 +135,17 @@ func jobMapper() map[string]map[string]JobDefinition { JobFunc: v2.RepairVM, TerminateFunc: leafJobVM.Build(), }, + + // GpuClaim + model.JobCreateGpuClaim: { + JobFunc: v2.CreateGpuClaim, + }, + model.JobDeleteGpuClaim: { + JobFunc: v2.DeleteGpuClaim, + }, + model.JobUpdateGpuClaim: { + JobFunc: v2.UpdateGpuClaim, + }, } return map[string]map[string]JobDefinition{ diff --git a/pkg/jobs/v2/gpu_claim_jobs.go b/pkg/jobs/v2/gpu_claim_jobs.go new file mode 100644 index 00000000..12cde3cd --- /dev/null +++ b/pkg/jobs/v2/gpu_claim_jobs.go @@ -0,0 +1,235 @@ +package v2 + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/pkg/db/resources/gpu_claim_repo" + jErrors "github.com/kthcloud/go-deploy/pkg/jobs/errors" + "github.com/kthcloud/go-deploy/pkg/jobs/utils" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra" + "github.com/kthcloud/go-deploy/service" + sErrors "github.com/kthcloud/go-deploy/service/errors" +) + +func CreateGpuClaim(job *model.Job) error { + err := utils.AssertParameters(job, []string{"id", "params"}) + if err != nil { + return jErrors.MakeTerminatedError(err) + } + + id := job.Args["id"].(string) + + paramsMap, ok := job.Args["params"].(map[string]any) + if !ok { + return jErrors.MakeTerminatedError(fmt.Errorf("invalid params type")) + } + + params, err := DecodeGpuClaimCreateParams(paramsMap) + if err != nil { + return jErrors.MakeTerminatedError(err) + } + + err = service.V2(utils.GetAuthInfo(job)).GpuClaims().Create(id, ¶ms) + if err != nil { + // We always terminate these jobs, since rerunning it would cause a ErrNonUniqueField + return jErrors.MakeTerminatedError(err) + } + + return nil +} + +func DeleteGpuClaim(job *model.Job) error { + err := utils.AssertParameters(job, []string{"id"}) + if err != nil { + return jErrors.MakeTerminatedError(err) + } + + id := job.Args["id"].(string) + + err = gpu_claim_repo.New().AddActivity(id, model.ActivityBeingDeleted) + if err != nil { + return jErrors.MakeTerminatedError(err) + } + + err = service.V2(utils.GetAuthInfo(job)).GpuClaims().Delete(id) + if err != nil { + if !errors.Is(err, sErrors.ErrResourceNotFound) { + return jErrors.MakeFailedError(err) + } + } + + return nil +} + +func UpdateGpuClaim(job *model.Job) error { + err := utils.AssertParameters(job, []string{"id", "params"}) + if err != nil { + return jErrors.MakeTerminatedError(err) + } + + id := job.Args["id"].(string) + + paramsMap, ok := job.Args["params"].(map[string]any) + if !ok { + return jErrors.MakeTerminatedError(fmt.Errorf("invalid params type")) + } + + params, err := DecodeGpuClaimUpdateParams(paramsMap) + if err != nil { + return jErrors.MakeTerminatedError(err) + } + if err := service.V2(utils.GetAuthInfo(job)).GpuClaims().Update(id, ¶ms); err != nil { + if errors.Is(err, sErrors.ErrResourceNotFound) { + return jErrors.MakeTerminatedError(err) + } + } + return nil +} + +// DecodeGpuClaimCreateParams decodes mapstructure for gpuClaimCreate. +// It is used because the gpuClaimCreateParams have such a complex format. +func DecodeGpuClaimCreateParams(raw map[string]any) (model.GpuClaimCreateParams, error) { + var result model.GpuClaimCreateParams + + type tempGpuDeviceConfiguration struct { + Driver string `json:"driver"` + Parameters any `json:"parameters"` + } + type tempRequestedGpu struct { + AllocationMode string `json:"allocationMode"` + DeviceClassName string `json:"deviceClassName"` + Name string `json:"name"` + Config *tempGpuDeviceConfiguration `json:"config,omitempty"` + } + type tempGpuClaimCreateParams struct { + Name string `json:"name"` + Zone string `json:"zone"` + Requested []tempRequestedGpu `json:"requested"` + AllowedRoles []string `json:"allowedRoles,omitempty"` + } + + var temp tempGpuClaimCreateParams + + data, err := json.Marshal(raw) + if err != nil { + return result, fmt.Errorf("failed to marshal raw input: %w", err) + } + if err := json.Unmarshal(data, &temp); err != nil { + return result, fmt.Errorf("failed to unmarshal into temp struct: %w", err) + } + + result.Name = temp.Name + result.Zone = temp.Zone + result.AllowedRoles = temp.AllowedRoles + result.Requested = make([]model.RequestedGpuCreate, len(temp.Requested)) + + for i, r := range temp.Requested { + req := model.RequestedGpuCreate{ + RequestedGpu: model.RequestedGpu{ + AllocationMode: model.RequestAllocationMode(r.AllocationMode), + DeviceClassName: r.DeviceClassName, + }, + + Name: r.Name, + } + + if r.Config != nil { + paramsJSON, err := json.Marshal(r.Config.Parameters) + if err != nil { + return result, fmt.Errorf("failed to marshal Parameters: %w", err) + } + + opaqueParams, err := parsers.Parse[dra.OpaqueParams](bytes.NewReader(paramsJSON)) + if err != nil { + return result, fmt.Errorf("failed to parse Parameters: %w", err) + } + + req.Config = &model.GpuDeviceConfiguration{ + Driver: r.Config.Driver, + Parameters: opaqueParams, + } + } + + result.Requested[i] = req + } + + return result, nil +} + +// DecodeGpuClaimUpdateParams decodes mapstructure for gpuClaimUpdate. +// It is used because the gpuClaimUpdateParams have such a complex format. +func DecodeGpuClaimUpdateParams(raw map[string]any) (model.GpuClaimUpdateParams, error) { + var result model.GpuClaimUpdateParams + + type tempGpuDeviceConfiguration struct { + Driver string `json:"driver"` + Parameters any `json:"parameters"` + } + type tempRequestedGpu struct { + AllocationMode string `json:"allocationMode"` + DeviceClassName string `json:"deviceClassName"` + Name string `json:"name"` + Config *tempGpuDeviceConfiguration `json:"config,omitempty"` + } + type tempGpuClaimCreateParams struct { + Name *string `json:"name,omitempty"` + Zone *string `json:"zone,omitempty"` + Requested *[]tempRequestedGpu `json:"requested,omitempty"` + AllowedRoles *[]string `json:"allowedRoles,omitempty"` + } + + var temp tempGpuClaimCreateParams + + data, err := json.Marshal(raw) + if err != nil { + return result, fmt.Errorf("failed to marshal raw input: %w", err) + } + if err := json.Unmarshal(data, &temp); err != nil { + return result, fmt.Errorf("failed to unmarshal into temp struct: %w", err) + } + + result.Name = temp.Name + result.Zone = temp.Zone + result.AllowedRoles = temp.AllowedRoles + if temp.Requested != nil { + resmap := make([]model.RequestedGpuCreate, len(*temp.Requested)) + result.Requested = &resmap + + for i, r := range *temp.Requested { + req := model.RequestedGpuCreate{ + RequestedGpu: model.RequestedGpu{ + AllocationMode: model.RequestAllocationMode(r.AllocationMode), + DeviceClassName: r.DeviceClassName, + }, + + Name: r.Name, + } + + if r.Config != nil { + paramsJSON, err := json.Marshal(r.Config.Parameters) + if err != nil { + return result, fmt.Errorf("failed to marshal Parameters: %w", err) + } + + opaqueParams, err := parsers.Parse[dra.OpaqueParams](bytes.NewReader(paramsJSON)) + if err != nil { + return result, fmt.Errorf("failed to parse Parameters: %w", err) + } + + req.Config = &model.GpuDeviceConfiguration{ + Driver: r.Config.Driver, + Parameters: opaqueParams, + } + } + + resmap[i] = req + } + } + + return result, nil +} diff --git a/pkg/services/cleaner/stale_resource_cleaner.go b/pkg/services/cleaner/stale_resource_cleaner.go index 32c52902..dea24c16 100644 --- a/pkg/services/cleaner/stale_resource_cleaner.go +++ b/pkg/services/cleaner/stale_resource_cleaner.go @@ -64,7 +64,6 @@ func staleResourceCleaner() error { err = service.V2().VMs().DoAction(vm.ID, &bodyV2.VmActionCreate{ Action: model.ActionStop, }) - if err != nil { return err } diff --git a/pkg/services/confirm/common.go b/pkg/services/confirm/common.go index 37e36cc7..b4eaaeb4 100644 --- a/pkg/services/confirm/common.go +++ b/pkg/services/confirm/common.go @@ -3,10 +3,11 @@ package confirm import ( "errors" "fmt" + "net" + "github.com/kthcloud/go-deploy/models/model" "github.com/kthcloud/go-deploy/pkg/config" "github.com/kthcloud/go-deploy/pkg/db/resources/vm_port_repo" - "net" ) // appDeletedK8s checks if the K8s setup for the given app is deleted. @@ -174,6 +175,17 @@ func portsCleared(vm *model.VM) (bool, error) { return !exists, nil } +// k8sDeleted checks if the K8s setup for the given GC is deleted. +func k8sDeletedGC(gc *model.GpuClaim) (bool, error) { + k8s := &gc.Subsystems.K8s + + if len(k8s.ResourceClaimMap) > 0 { + return false, nil + } + + return !k8s.Namespace.Created(), nil +} + // checkCustomDomain checks if the custom domain is setup. // This polls the TXT record of the custom domain to check if the secret is set. // It returns (exists, match, txtRecord, error). diff --git a/pkg/services/confirm/gc_confirmer.go b/pkg/services/confirm/gc_confirmer.go new file mode 100644 index 00000000..8b8916ed --- /dev/null +++ b/pkg/services/confirm/gc_confirmer.go @@ -0,0 +1,45 @@ +package confirm + +import ( + slices0 "slices" + + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/pkg/db/resources/gpu_claim_repo" + "github.com/kthcloud/go-deploy/pkg/db/resources/job_repo" + "github.com/kthcloud/go-deploy/pkg/log" +) + +// GcDeletionConfirmer is a worker that confirms GC deletion. +// It checks if each subsystem resource is deleted, and if any related jobs are finished. +func GcDeletionConfirmer() error { + beingDeleted, err := gpu_claim_repo.New().WithActivities(model.ActivityBeingDeleted).List() + if err != nil { + return err + } + + for _, gc := range beingDeleted { + deleted := GCDeleted(&gc) + if !deleted { + continue + } + + relatedJobs, err := job_repo.New().ExcludeScheduled().FilterArgs("id", gc.ID).List() + if err != nil { + return err + } + + allFinished := slices0.IndexFunc(([]model.Job)(relatedJobs), (func(model.Job) bool)(func(j model.Job) bool { + return j.Status != model.JobStatusCompleted && j.Status != model.JobStatusTerminated + })) == -1 + + if allFinished { + log.Printf("Marking GC %s as deleted", gc.ID) + err = gpu_claim_repo.New().DeleteByID(gc.ID) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/services/confirm/service.go b/pkg/services/confirm/service.go index 5d6d2312..33cab0ec 100644 --- a/pkg/services/confirm/service.go +++ b/pkg/services/confirm/service.go @@ -2,6 +2,7 @@ package confirm import ( "context" + "github.com/kthcloud/go-deploy/pkg/config" "github.com/kthcloud/go-deploy/pkg/log" "github.com/kthcloud/go-deploy/pkg/services" @@ -16,4 +17,5 @@ func Setup(ctx context.Context) { go services.PeriodicWorker(ctx, "smDeletionConfirmer", SmDeletionConfirmer, config.Config.Timer.SmDeletionConfirm) go services.PeriodicWorker(ctx, "vmDeletionConfirmer", VmDeletionConfirmer, config.Config.Timer.VmDeletionConfirm) go services.PeriodicWorker(ctx, "customDomainConfirmer", CustomDomainConfirmer, config.Config.Timer.CustomDomainConfirm) + go services.PeriodicWorker(ctx, "gpClaimDeletionConfirmer", GcDeletionConfirmer, config.Config.Timer.DeploymentDeletionConfirm) } diff --git a/pkg/services/confirm/subsystems.go b/pkg/services/confirm/subsystems.go index a09e33af..562816fd 100644 --- a/pkg/services/confirm/subsystems.go +++ b/pkg/services/confirm/subsystems.go @@ -25,6 +25,13 @@ func getVmDeletedConfirmers() []func(*model.VM) (bool, error) { } } +// getGCDeletedConfirmers gets the confirmers for GC deletion. +func getGCDeletedConfirmers() []func(*model.GpuClaim) (bool, error) { + return []func(*model.GpuClaim) (bool, error){ + k8sDeletedGC, + } +} + // DeploymentDeleted checks if the deployment is deleted by checking all confirmers. func DeploymentDeleted(deployment *model.Deployment) bool { confirmers := getDeploymentDeletedConfirmers() @@ -60,3 +67,15 @@ func VmDeleted(vm *model.VM) bool { } return true } + +// GCDeleted checks if the GC is deleted by checking all confirmers. +func GCDeleted(gc *model.GpuClaim) bool { + confirmers := getGCDeletedConfirmers() + for _, confirmer := range confirmers { + deleted, _ := confirmer(gc) + if !deleted { + return false + } + } + return true +} diff --git a/pkg/services/status_update/gpu_claim_status_listener.go b/pkg/services/status_update/gpu_claim_status_listener.go new file mode 100644 index 00000000..393b690f --- /dev/null +++ b/pkg/services/status_update/gpu_claim_status_listener.go @@ -0,0 +1,84 @@ +package status_update + +import ( + "context" + "fmt" + "time" + + configModels "github.com/kthcloud/go-deploy/models/config" + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/pkg/config" + "github.com/kthcloud/go-deploy/pkg/db/resources/gpu_claim_repo" + "github.com/kthcloud/go-deploy/pkg/log" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/models" + "github.com/kthcloud/go-deploy/service/v2/gpu_claims/k8s_service" + "github.com/kthcloud/go-deploy/utils" +) + +func GpuClaimStatusListener(ctx context.Context) error { + for _, zone := range config.Config.EnabledZones() { + if !zone.HasCapability(configModels.ZoneCapabilityDeployment) && !zone.HasCapability(configModels.ZoneCapabilityDRA) { + continue + } + + log.Println("Setting up gpu claim status watcher for zone", zone.Name) + + z := zone + err := k8s_service.New().SetupResourceClaimWatcher(ctx, &z, func(name string, status models.ResourceClaimStatus, action string) { + log.Println("New gpu claim event!") + + err := gpu_claim_repo.New().ReconcileStateByName(name, + gpu_claim_repo.WithSetStatus(parseGpuClaimStatus(status)), + gpu_claim_repo.WithSetConsumers(convertGpuClaimConsumers(status.Consumers)), + gpu_claim_repo.WithSetAllocated(convertGpuClaimAllocations(status.AllocationResults)), + ) + if err != nil { + // will happen if the resourceclaim isnt in the db + log.Println("Failed to set reconcile state for gpu claim", name, "details:", err) + return + } + }) + if err != nil { + return fmt.Errorf("failed to set up gpu claim state reconciler for zone %s. details: %w", zone.Name, err) + } + } + return nil +} + +func parseGpuClaimStatus(status models.ResourceClaimStatus) *model.GpuClaimStatus { + return &model.GpuClaimStatus{ + Phase: func() model.GpuClaimStatusPhase { + if status.Allocated { + return model.GpuClaimStatusPhase_Bound + } + return model.GpuClaimStatusPhase_Pending + }(), + LastSynced: utils.PtrOf(time.Now()), + } +} + +func convertGpuClaimConsumers(consumers []models.ResourceClaimConsumerPublic) []model.GpuClaimConsumer { + res := make([]model.GpuClaimConsumer, len(consumers)) + for i, consumer := range consumers { + res[i] = model.GpuClaimConsumer{ + APIGroup: consumer.APIGroup, + Name: consumer.Name, + Resource: consumer.Resource, + UID: consumer.UID, + } + } + return res +} + +func convertGpuClaimAllocations(allocations []models.ResourceClaimAllocationResultPublic) map[string][]model.AllocatedGpu { + res := make(map[string][]model.AllocatedGpu) + for _, alloc := range allocations { + res[alloc.Request] = append(res[alloc.Request], model.AllocatedGpu{ + Pool: alloc.Pool, + Device: alloc.Device, + ShareID: alloc.ShareID, + AdminAccess: alloc.AdminAccess, + }) + } + return res +} diff --git a/pkg/services/status_update/service.go b/pkg/services/status_update/service.go index 36ee9f9f..a1fd2e69 100644 --- a/pkg/services/status_update/service.go +++ b/pkg/services/status_update/service.go @@ -2,6 +2,7 @@ package status_update import ( "context" + "github.com/kthcloud/go-deploy/pkg/config" "github.com/kthcloud/go-deploy/pkg/log" "github.com/kthcloud/go-deploy/pkg/services" @@ -14,6 +15,7 @@ func Setup(ctx context.Context) { go services.Worker(ctx, "deploymentStatusListener", DeploymentStatusListener) go services.Worker(ctx, "vmStatusListener", VmStatusListener) go services.Worker(ctx, "eventListener", DeploymentEventListener) + go services.Worker(ctx, "gpuClaimStatusListener", GpuClaimStatusListener) go services.PeriodicWorker(ctx, "deploymentStatusFetcher", DeploymentStatusFetcher, config.Config.Timer.DeploymentStatusUpdate) go services.PeriodicWorker(ctx, "vmStatusFetcher", VmStatusFetcher, config.Config.Timer.VmStatusUpdate) diff --git a/pkg/subsystems/k8s/api/cmd/structclone/main.go b/pkg/subsystems/k8s/api/cmd/structclone/main.go new file mode 100644 index 00000000..d7e1b837 --- /dev/null +++ b/pkg/subsystems/k8s/api/cmd/structclone/main.go @@ -0,0 +1,239 @@ +//go:build ignore + +package main + +import ( + "fmt" + "go/format" + "os" + "reflect" + "strings" + + nvresourcebetav1 "github.com/NVIDIA/k8s-dra-driver-gpu/api/nvidia.com/resource/v1beta1" +) + +func main() { + + // Generate everything required for the nvidia gpuConfig struct + // This is done so we dont have to rely on their package as + // a runtime depencency. + // Why would we not want to use it? + // Primarily they have a `func init()` that requires a compile time + // flag to be set to a specific version for their library to work + // they use it as a featuregate, but it is not feasable to have to + // set that flag just to be able to use one struct from their code. + // + // Hence this generation code has been made, so we get our own struct + // that we can use to bind the opaque params to. + generateRecursive(nvresourcebetav1.GpuConfig{}, "nvidia") + +} + +func generateRecursive(v any, pkgName string) { + visited := map[string]bool{} + t := reflect.TypeOf(v) + generateType(t, pkgName, t.PkgPath(), visited) +} + +func generateType(t reflect.Type, pkgName string, targetPkgPrefix string, visited map[string]bool) { + if t == nil { + return + } + + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + if t.Name() == "" { + return + } + + fullName := t.PkgPath() + "." + t.Name() + if visited[fullName] { + return + } + visited[fullName] = true + + isLocal := strings.HasPrefix(t.PkgPath(), targetPkgPrefix) + if !isLocal && t.PkgPath() != "" { + return // external type, skip + } + + switch t.Kind() { + case reflect.Struct: + imports := map[string]string{} + structDef := renderStruct(t, 0, imports, pkgName, targetPkgPrefix, visited) + + var importLines []string + for alias, path := range imports { + if strings.HasPrefix(path, targetPkgPrefix) { + continue + } + importLines = append(importLines, fmt.Sprintf("\t%s \"%s\"", alias, path)) + } + importSection := "" + if len(importLines) > 0 { + importSection = "import (\n" + strings.Join(importLines, "\n") + "\n)\n\n" + } + + code := fmt.Sprintf(`// Code generated by go:generate with kthcloud/go-deploy/pkg/subsystems/k8s/api/cmd/structclone; DO NOT EDIT. + +package %s + +%s +type %s %s +`, pkgName, importSection, t.Name(), structDef) + + writeFile(pkgName, t.Name(), code) + fmt.Println("Generated struct:", t.Name()) + + default: + base := resolveUnderlyingType(t, visited, pkgName, targetPkgPrefix) + code := fmt.Sprintf(`// Code generated by go:generate with kthcloud/go-deploy/pkg/subsystems/k8s/api/cmd/structclone; DO NOT EDIT. + +package %s + +type %s %s +`, pkgName, t.Name(), base) + + writeFile(pkgName, t.Name(), code) + fmt.Println("Generated alias:", t.Name()) + } +} + +func renderStruct(t reflect.Type, indent int, imports map[string]string, pkgName string, targetPkgPrefix string, visited map[string]bool) string { + var b strings.Builder + ind := strings.Repeat("\t", indent) + b.WriteString("struct {\n") + + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if f.PkgPath != "" { + continue + } + + name := f.Name + fieldType := typeString(f.Type, imports, pkgName, targetPkgPrefix, visited) + tag := f.Tag.Get("json") + if tag == "" { + tag = strings.ToLower(name) + } + if strings.Contains(tag, "inline") { + // if it is inline we embed it + name = "" + } + + b.WriteString(fmt.Sprintf("%s\t%s %s `json:\"%s\"`\n", ind+"\t", name, fieldType, tag)) + } + + b.WriteString(ind + "}") + return b.String() +} + +func typeString(t reflect.Type, imports map[string]string, pkgName string, targetPkgPrefix string, visited map[string]bool) string { + if t == nil { + return "interface{}" + } + + switch t.Kind() { + case reflect.Pointer: + return "*" + typeString(t.Elem(), imports, pkgName, targetPkgPrefix, visited) + case reflect.Slice: + return "[]" + typeString(t.Elem(), imports, pkgName, targetPkgPrefix, visited) + case reflect.Array: + return fmt.Sprintf("[%d]%s", t.Len(), typeString(t.Elem(), imports, pkgName, targetPkgPrefix, visited)) + case reflect.Map: + return fmt.Sprintf("map[%s]%s", typeString(t.Key(), imports, pkgName, targetPkgPrefix, visited), typeString(t.Elem(), imports, pkgName, targetPkgPrefix, visited)) + case reflect.Struct: + if strings.HasPrefix(t.PkgPath(), targetPkgPrefix) { + generateType(t, pkgName, targetPkgPrefix, visited) + return t.Name() + } + + if t.PkgPath() != "" { + parts := strings.Split(t.PkgPath(), "/") + pkgAlias := parts[len(parts)-1] + imports[pkgAlias] = t.PkgPath() + return fmt.Sprintf("%s.%s", pkgAlias, t.Name()) + } + return t.Name() + + default: + if strings.HasPrefix(t.PkgPath(), targetPkgPrefix) { + generateType(t, pkgName, targetPkgPrefix, visited) + return t.Name() + } + + if t.PkgPath() != "" { + parts := strings.Split(t.PkgPath(), "/") + pkgAlias := parts[len(parts)-1] + imports[pkgAlias] = t.PkgPath() + return fmt.Sprintf("%s.%s", pkgAlias, t.Name()) + } + + return t.Name() + } +} + +// resolveUnderlyingType returns the real Go type (e.g. "string", "int", etc.) +// for alias-like types (GpuSharingStrategy => string) +func resolveUnderlyingType(t reflect.Type, visited map[string]bool, pkgName string, targetPkgPrefix string) string { + // Deref pointers + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + + // Try to find base kind + switch t.Kind() { + case reflect.Struct: + return t.Name() + case reflect.Slice: + return "[]" + resolveUnderlyingType(t.Elem(), visited, pkgName, targetPkgPrefix) + case reflect.Map: + return fmt.Sprintf("map[%s]%s", + resolveUnderlyingType(t.Key(), visited, pkgName, targetPkgPrefix), + resolveUnderlyingType(t.Elem(), visited, pkgName, targetPkgPrefix)) + case reflect.String: + return "string" + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return "int" + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return "uint" + case reflect.Float32: + return "float32" + case reflect.Float64: + return "float64" + case reflect.Interface: + return "interface{}" + default: + // Try to generate nested type if local + if strings.HasPrefix(t.PkgPath(), targetPkgPrefix) { + generateType(t, pkgName, targetPkgPrefix, visited) + return t.Name() + } + // External alias, import required + if t.PkgPath() != "" { + parts := strings.Split(t.PkgPath(), "/") + pkgAlias := parts[len(parts)-1] + return fmt.Sprintf("%s.%s", pkgAlias, t.Name()) + } + return t.Kind().String() + } +} + +func writeFile(pkgName, name, code string) { + os.MkdirAll(pkgName, 0755) + outPath := fmt.Sprintf("%s/%s.go", pkgName, strings.ToLower(name)) + + // Run gofmt on the generated code + formatted, err := format.Source([]byte(code)) + if err != nil { + fmt.Fprintf(os.Stderr, "warning: could not format %s: %v\n", outPath, err) + formatted = []byte(code) + } + + if err := os.WriteFile(outPath, formatted, 0644); err != nil { + panic(err) + } +} diff --git a/pkg/subsystems/k8s/api/generate.go b/pkg/subsystems/k8s/api/generate.go new file mode 100644 index 00000000..46851023 --- /dev/null +++ b/pkg/subsystems/k8s/api/generate.go @@ -0,0 +1,10 @@ +// This will generate api types so we dont have to have them as runtime deps. +// As you can see below, nvidias dra package requires a flag to be set to a version +// we dont want to have to do this when we compile go-deploy. +// Thus the struct that we depend on from that package is generated as a copy, by +// parsing the json tags of said struct. This is controlled in the +// ./cmd/structclone/main.go file, it is extendable and more structs can be added if wanted. + +//go:generate go run -ldflags=-X=github.com/NVIDIA/k8s-dra-driver-gpu/internal/info.version=v1.34.2 ./cmd/structclone/main.go + +package api diff --git a/pkg/subsystems/k8s/api/nvidia/gpuconfig.go b/pkg/subsystems/k8s/api/nvidia/gpuconfig.go new file mode 100644 index 00000000..6ad9cdc3 --- /dev/null +++ b/pkg/subsystems/k8s/api/nvidia/gpuconfig.go @@ -0,0 +1,12 @@ +// Code generated by go:generate with kthcloud/go-deploy/pkg/subsystems/k8s/api/cmd/structclone; DO NOT EDIT. + +package nvidia + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GpuConfig struct { + v1.TypeMeta `json:",inline" bson:",inline"` + Sharing *GpuSharing `json:"sharing,omitempty" bson:"sharing,omitempty"` +} diff --git a/pkg/subsystems/k8s/api/nvidia/gpusharing.go b/pkg/subsystems/k8s/api/nvidia/gpusharing.go new file mode 100644 index 00000000..2cc710df --- /dev/null +++ b/pkg/subsystems/k8s/api/nvidia/gpusharing.go @@ -0,0 +1,9 @@ +// Code generated by go:generate with kthcloud/go-deploy/pkg/subsystems/k8s/api/cmd/structclone; DO NOT EDIT. + +package nvidia + +type GpuSharing struct { + Strategy GpuSharingStrategy `json:"strategy"` + TimeSlicingConfig *TimeSlicingConfig `json:"timeSlicingConfig,omitempty"` + MpsConfig *MpsConfig `json:"mpsConfig,omitempty"` +} diff --git a/pkg/subsystems/k8s/api/nvidia/gpusharingstrategy.go b/pkg/subsystems/k8s/api/nvidia/gpusharingstrategy.go new file mode 100644 index 00000000..04623e6a --- /dev/null +++ b/pkg/subsystems/k8s/api/nvidia/gpusharingstrategy.go @@ -0,0 +1,5 @@ +// Code generated by go:generate with kthcloud/go-deploy/pkg/subsystems/k8s/api/cmd/structclone; DO NOT EDIT. + +package nvidia + +type GpuSharingStrategy string diff --git a/pkg/subsystems/k8s/api/nvidia/mpsconfig.go b/pkg/subsystems/k8s/api/nvidia/mpsconfig.go new file mode 100644 index 00000000..1018444e --- /dev/null +++ b/pkg/subsystems/k8s/api/nvidia/mpsconfig.go @@ -0,0 +1,13 @@ +// Code generated by go:generate with kthcloud/go-deploy/pkg/subsystems/k8s/api/cmd/structclone; DO NOT EDIT. + +package nvidia + +import ( + resource "k8s.io/apimachinery/pkg/api/resource" +) + +type MpsConfig struct { + DefaultActiveThreadPercentage *int `json:"defaultActiveThreadPercentage,omitempty"` + DefaultPinnedDeviceMemoryLimit *resource.Quantity `json:"defaultPinnedDeviceMemoryLimit,omitempty"` + DefaultPerDevicePinnedMemoryLimit map[string]resource.Quantity `json:"defaultPerDevicePinnedMemoryLimit,omitempty"` +} diff --git a/pkg/subsystems/k8s/api/nvidia/timesliceinterval.go b/pkg/subsystems/k8s/api/nvidia/timesliceinterval.go new file mode 100644 index 00000000..24fb69e8 --- /dev/null +++ b/pkg/subsystems/k8s/api/nvidia/timesliceinterval.go @@ -0,0 +1,5 @@ +// Code generated by go:generate with kthcloud/go-deploy/pkg/subsystems/k8s/api/cmd/structclone; DO NOT EDIT. + +package nvidia + +type TimeSliceInterval string diff --git a/pkg/subsystems/k8s/api/nvidia/timeslicingconfig.go b/pkg/subsystems/k8s/api/nvidia/timeslicingconfig.go new file mode 100644 index 00000000..80e92cef --- /dev/null +++ b/pkg/subsystems/k8s/api/nvidia/timeslicingconfig.go @@ -0,0 +1,7 @@ +// Code generated by go:generate with kthcloud/go-deploy/pkg/subsystems/k8s/api/cmd/structclone; DO NOT EDIT. + +package nvidia + +type TimeSlicingConfig struct { + Interval *TimeSliceInterval `json:"interval,omitempty"` +} diff --git a/pkg/subsystems/k8s/manifests.go b/pkg/subsystems/k8s/manifests.go index d745bd37..13bc3edc 100644 --- a/pkg/subsystems/k8s/manifests.go +++ b/pkg/subsystems/k8s/manifests.go @@ -1,6 +1,7 @@ package k8s import ( + "encoding/json" "fmt" "strings" "time" @@ -8,13 +9,16 @@ import ( "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/keys" "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/models" "github.com/kthcloud/go-deploy/utils" + "github.com/kthcloud/go-deploy/utils/hashutils" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" v1 "k8s.io/api/batch/v1" apiv1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + resourcev1 "k8s.io/api/resource/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" kubevirtv1 "kubevirt.io/api/core/v1" snapshotalpha1 "kubevirt.io/api/snapshot/v1alpha1" @@ -62,8 +66,8 @@ func CreateDeploymentManifest(public *models.DeploymentPublic) *appsv1.Deploymen } } - volumes := make([]apiv1.Volume, 0) - usedNames := make(map[string]bool) + volumes := make([]apiv1.Volume, 0, len(public.Volumes)) + usedNames := make(map[string]bool, len(public.Volumes)) for _, volume := range public.Volumes { if usedNames[volume.Name] { continue @@ -106,6 +110,61 @@ func CreateDeploymentManifest(public *models.DeploymentPublic) *appsv1.Deploymen } } + resourceClaims := make([]apiv1.PodResourceClaim, 0, len(public.ResourceClaims)) + usedResourceClaimName := make(map[string]bool, len(public.ResourceClaims)) + for _, resourceClaim := range public.ResourceClaims { + if usedResourceClaimName[resourceClaim.Name] { + continue + } + usedResourceClaimName[resourceClaim.Name] = true + + rc := apiv1.PodResourceClaim{ + Name: resourceClaim.Name, + } + + if resourceClaim.ResourceClaimTemplateName != nil { + rc.ResourceClaimTemplateName = utils.StrPtr(*resourceClaim.ResourceClaimTemplateName) + } else if resourceClaim.ResourceClaimName != nil { + rc.ResourceClaimName = utils.StrPtr(*resourceClaim.ResourceClaimName) + } else { + continue + } + + resourceClaims = append(resourceClaims, rc) + } + + tolerations := make([]apiv1.Toleration, 0, len(public.Tolerations)) + for _, toleration := range public.Tolerations { + tolerations = append(tolerations, apiv1.Toleration{ + Key: toleration.Key, + Operator: apiv1.TolerationOperator(toleration.Operator), + Effect: apiv1.TaintEffect(toleration.Effect), + }) + } + + // Add toleration if there is a resourceclaim + // TODO: make this more dynamic + if len(resourceClaims) > 0 && len(tolerations) < 1 { + tolerations = append(tolerations, apiv1.Toleration{ + Key: "nvidia.com/gpu", + Operator: apiv1.TolerationOperator("Exists"), + Effect: apiv1.TaintEffect("NoSchedule"), + }) + } + + // will most likely have only one req per claim, thus we allocate that size + // on the off-chance we need more, we can accept the extra time needed to expand + resourceClaimUsages := make([]apiv1.ResourceClaim, 0, len(public.ResourceClaims)) + + for _, claim := range public.ResourceClaims { + for _, req := range claim.Request { + resourceClaimUsages = append(resourceClaimUsages, apiv1.ResourceClaim{ + Name: claim.Name, + Request: req, + }) + } + } + initContainers := make([]apiv1.Container, len(public.InitContainers)) for i, initContainer := range public.InitContainers { initContainers[i] = apiv1.Container{ @@ -161,7 +220,9 @@ func CreateDeploymentManifest(public *models.DeploymentPublic) *appsv1.Deploymen }, }, Spec: apiv1.PodSpec{ - Volumes: volumes, + Volumes: volumes, + ResourceClaims: resourceClaims, + Tolerations: tolerations, Containers: []apiv1.Container{ { Name: public.Name, @@ -172,6 +233,7 @@ func CreateDeploymentManifest(public *models.DeploymentPublic) *appsv1.Deploymen Resources: apiv1.ResourceRequirements{ Limits: limits, Requests: requests, + Claims: resourceClaimUsages, }, Lifecycle: lifecycle, VolumeMounts: normalContainerMounts, @@ -788,6 +850,135 @@ func CreateNetworkPolicyManifest(public *models.NetworkPolicyPublic) *networking } } +func CreateResourceClaimManifest(public *models.ResourceClaimPublic) *resourcev1.ResourceClaim { + + var deviceClaim resourcev1.DeviceClaim = resourcev1.DeviceClaim{ + Requests: make([]resourcev1.DeviceRequest, 0, len(public.DeviceRequests)), + // TODO: add constraints + //Constraints: []resourcev1.DeviceConstraint{}, + Config: make([]resourcev1.DeviceClaimConfiguration, 0, len(public.DeviceRequests)), + } + + cfgReqMap := make(map[string][]string, len(public.DeviceRequests)) + for _, req := range public.DeviceRequests { + if req.Config != nil { + + raw, err := json.Marshal(req.Config.Parameters) + if err != nil { + // TODO: handle somwhow + continue + } + dcc := resourcev1.DeviceClaimConfiguration{ + DeviceConfiguration: resourcev1.DeviceConfiguration{ + Opaque: &resourcev1.OpaqueDeviceConfiguration{ + Driver: req.Config.Driver, + Parameters: runtime.RawExtension{ + Raw: raw, + // TODO: test if this is needed + //Object: req.Config.Parameters, + }, + }, + }, + } + + key, _ := hashutils.HashDeterministicJSON(dcc.DeviceConfiguration) + if strings.TrimSpace(key) == "" { + continue + } + if v, found := cfgReqMap[key]; found { + cfgReqMap[key] = append(v, req.Name) + // already present + continue + } else { + cfgReqMap[key] = []string{req.Name} + } + + deviceClaim.Config = append(deviceClaim.Config, dcc) + } + + var dr resourcev1.DeviceRequest + + dr.Name = req.Name + + if req.RequestsExactly != nil { + dr.Exactly = &resourcev1.ExactDeviceRequest{ + DeviceClassName: req.RequestsExactly.DeviceClassName, + AllocationMode: resourcev1.DeviceAllocationMode(req.RequestsExactly.AllocationMode), + Count: req.RequestsExactly.Count, + AdminAccess: req.RequestsExactly.AdminAccess, + Capacity: &resourcev1.CapacityRequirements{ + Requests: req.RequestsExactly.CapacityRequests, + }, + // TODO: add tolerations + } + for _, sel := range req.RequestsExactly.SelectorCelExprs { + dr.Exactly.Selectors = append(dr.Exactly.Selectors, resourcev1.DeviceSelector{ + CEL: &resourcev1.CELDeviceSelector{ + Expression: sel, + }, + }) + } + } + + if len(req.RequestsFirstAvaliable) > 0 { + for _, fa := range req.RequestsFirstAvaliable { + dsr := resourcev1.DeviceSubRequest{ + DeviceClassName: fa.DeviceClassName, + AllocationMode: resourcev1.DeviceAllocationMode(fa.AllocationMode), + Count: fa.Count, + Capacity: &resourcev1.CapacityRequirements{ + Requests: fa.CapacityRequests, + }, + // TODO: add tolerations + } + for _, sel := range fa.SelectorCelExprs { + dsr.Selectors = append(dsr.Selectors, resourcev1.DeviceSelector{ + CEL: &resourcev1.CELDeviceSelector{ + Expression: sel, + }, + }) + } + dr.FirstAvailable = append(dr.FirstAvailable, dsr) + } + } + + deviceClaim.Requests = append(deviceClaim.Requests, dr) + } + + for i, cfg := range deviceClaim.Config { + if cfg.Opaque == nil { + continue + } + key, _ := hashutils.HashDeterministicJSON(cfg.DeviceConfiguration) + if strings.TrimSpace(key) == "" { + continue + } + if v, found := cfgReqMap[key]; found { + deviceClaim.Config[i].Requests = v + } else { + // Public input contains a config without any requests, + // just leave it in for now, k8s will probably return error for it + // TODO: check this behavior + } + } + + return &resourcev1.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: public.Name, + Namespace: public.Namespace, + Labels: map[string]string{ + keys.LabelDeployName: public.Name, + }, + Annotations: map[string]string{ + keys.AnnotationCreationTimestamp: public.CreatedAt.Format(timeFormat), + }, + }, + Spec: resourcev1.ResourceClaimSpec{ + Devices: deviceClaim, + }, + } +} + // pathTypeAddr is a helper function to convert a string to a networkingv1.PathType. func pathTypeAddr(s string) *networkingv1.PathType { return (*networkingv1.PathType)(&s) diff --git a/pkg/subsystems/k8s/models/common.go b/pkg/subsystems/k8s/models/common.go index 446be418..c6cad23f 100644 --- a/pkg/subsystems/k8s/models/common.go +++ b/pkg/subsystems/k8s/models/common.go @@ -1,9 +1,10 @@ package models import ( + "time" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/keys" v1 "k8s.io/api/core/v1" - "time" ) type K8sResource interface { @@ -44,6 +45,19 @@ type Resources struct { Requests Requests `bson:"requests"` } +type DynamicResourceClaim struct { + Name string `bson:"name"` + Request []string `bson:"request,omitempty"` + ResourceClaimName *string `bson:"resourceClaimName,omitempty"` + ResourceClaimTemplateName *string `bson:"resourceClaimTemplateName,omitempty"` +} + +type Toleration struct { + Key string `bson:"name"` + Operator string `bson:"operator"` + Effect string `bson:"effect"` +} + // ToK8sEnvVar converts an EnvVar to a v1.EnvVar. func (envVar *EnvVar) ToK8sEnvVar() v1.EnvVar { return v1.EnvVar{ diff --git a/pkg/subsystems/k8s/models/deployment_public.go b/pkg/subsystems/k8s/models/deployment_public.go index 5ea293f1..bfb4935e 100644 --- a/pkg/subsystems/k8s/models/deployment_public.go +++ b/pkg/subsystems/k8s/models/deployment_public.go @@ -1,8 +1,9 @@ package models import ( - appsv1 "k8s.io/api/apps/v1" "time" + + appsv1 "k8s.io/api/apps/v1" ) type DeploymentPublic struct { @@ -10,16 +11,18 @@ type DeploymentPublic struct { Namespace string `bson:"namespace"` Labels map[string]string `bson:"labels"` - Image string `bson:"image"` - ImagePullSecrets []string `bson:"imagePullSecrets"` - EnvVars []EnvVar `bson:"envVars"` - Resources Resources `bson:"resources"` - Command []string `bson:"command"` - Args []string `bson:"args"` - InitCommands []string `bson:"initCommands"` - InitContainers []InitContainer `bson:"initContainers"` - Volumes []Volume `bson:"volumes"` - CreatedAt time.Time `bson:"createdAt"` + Image string `bson:"image"` + ImagePullSecrets []string `bson:"imagePullSecrets"` + EnvVars []EnvVar `bson:"envVars"` + Resources Resources `bson:"resources"` + Command []string `bson:"command"` + Args []string `bson:"args"` + InitCommands []string `bson:"initCommands"` + InitContainers []InitContainer `bson:"initContainers"` + Volumes []Volume `bson:"volumes"` + ResourceClaims []DynamicResourceClaim `bson:"resourcClaims,omitempty"` + Tolerations []Toleration `bson:"tolerations,omitempty"` + CreatedAt time.Time `bson:"createdAt"` // Disabled is a flag that can be set to true to disable the deployment. // This is useful for deployments that should not be running, but should still exist. @@ -48,6 +51,8 @@ func CreateDeploymentPublicFromRead(deployment *appsv1.Deployment) *DeploymentPu var command []string var args []string var volumes []Volume + var claims []DynamicResourceClaim + var tolerations []Toleration var image string for _, k8sVolume := range deployment.Spec.Template.Spec.Volumes { @@ -63,6 +68,35 @@ func CreateDeploymentPublicFromRead(deployment *appsv1.Deployment) *DeploymentPu }) } + for _, k8sResourceClaim := range deployment.Spec.Template.Spec.ResourceClaims { + var ( + resourceClaimTemplateName, resourceClaimName *string + ) + + if k8sResourceClaim.ResourceClaimTemplateName != nil { + resourceClaimTemplateName = k8sResourceClaim.ResourceClaimName + } + + if k8sResourceClaim.ResourceClaimName != nil { + resourceClaimName = k8sResourceClaim.ResourceClaimName + } + + claims = append(claims, DynamicResourceClaim{ + Name: k8sResourceClaim.Name, + ResourceClaimName: resourceClaimName, + ResourceClaimTemplateName: resourceClaimTemplateName, + }) + } + + // TODO: figure out how this can be done better + for _, k8sToleration := range deployment.Spec.Template.Spec.Tolerations { + tolerations = append(tolerations, Toleration{ + Key: k8sToleration.Key, + Operator: string(k8sToleration.Operator), + Effect: string(k8sToleration.Effect), + }) + } + if len(deployment.Spec.Template.Spec.Containers) > 0 { firstContainer := deployment.Spec.Template.Spec.Containers[0] resources := firstContainer.Resources @@ -103,6 +137,15 @@ func CreateDeploymentPublicFromRead(deployment *appsv1.Deployment) *DeploymentPu } } } + + for _, resourceClaim := range resources.Claims { + for idx, claim := range claims { + if claim.Name == resourceClaim.Name { + claims[idx].Request = append(claims[idx].Request, resourceClaim.Request) + break + } + } + } } imagePullSecrets := make([]string, len(deployment.Spec.Template.Spec.ImagePullSecrets)) @@ -164,6 +207,8 @@ func CreateDeploymentPublicFromRead(deployment *appsv1.Deployment) *DeploymentPu InitCommands: initCommands, InitContainers: initContainers, Volumes: volumes, + ResourceClaims: claims, + Tolerations: tolerations, CreatedAt: formatCreatedAt(deployment.Annotations), } } diff --git a/pkg/subsystems/k8s/models/resourceclaim_public.go b/pkg/subsystems/k8s/models/resourceclaim_public.go new file mode 100644 index 00000000..c11f742f --- /dev/null +++ b/pkg/subsystems/k8s/models/resourceclaim_public.go @@ -0,0 +1,252 @@ +package models + +import ( + "bytes" + "fmt" + "slices" + "time" + + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra/nvidia" + "go.mongodb.org/mongo-driver/bson" + resourcev1 "k8s.io/api/resource/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +type ResourceClaimPublic struct { + Name string `bson:"name"` + Namespace string `bson:"namespace"` + DeviceRequests []ResourceClaimDeviceRequestPublic `bson:"deviceRequests"` + Allocated bool `bson:"allocated"` + AllocationResults []ResourceClaimAllocationResultPublic `bson:"allocationResults,omitempty"` + Consumers []ResourceClaimConsumerPublic `bson:"consumers,omitempty"` + CreatedAt time.Time `bson:"createdAt"` +} + +type ResourceClaimDeviceRequestPublic struct { + Name string `bson:"name"` + RequestsExactly *ResourceClaimExactlyRequestPublic `bson:"requestExactly,omitempty"` + RequestsFirstAvaliable []ResourceClaimBaseRequestPublic `bson:"requestFirstAvailable,omitempty"` + Config *ResourceClaimOpaquePublic `bson:"opaque,omitempty"` +} + +type ResourceClaimBaseRequestPublic struct { + AllocationMode string `bson:"allocationMode"` + CapacityRequests map[resourcev1.QualifiedName]resource.Quantity `bson:"capacityRequests,omitempty"` + Count int64 `bson:"count,omitempty"` + DeviceClassName string `bson:"deviceClassName"` + SelectorCelExprs []string `bson:"selectorCelExprs,omitempty"` + //TODO: Tolerations +} + +type ResourceClaimExactlyRequestPublic struct { + ResourceClaimBaseRequestPublic `bson:",inline"` + AdminAccess *bool `bson:"adminAccess,omitempty"` +} + +type ResourceClaimOpaquePublic struct { + Driver string `bson:"driver"` + Parameters dra.OpaqueParams `bson:"parameters,omitempty"` +} + +func (cfg ResourceClaimOpaquePublic) MarshalBSON() ([]byte, error) { + doc := bson.M{ + "driver": cfg.Driver, + } + + if cfg.Parameters != nil { + // Marshal interface to BSON using its JSON form (to preserve structure) + paramBytes, err := bson.Marshal(cfg.Parameters) + if err != nil { + return nil, fmt.Errorf("failed to marshal parameters: %w", err) + } + + var paramDoc bson.M + if err := bson.Unmarshal(paramBytes, ¶mDoc); err != nil { + return nil, err + } + doc["parameters"] = paramDoc + } + + return bson.Marshal(doc) +} + +func (cfg *ResourceClaimOpaquePublic) UnmarshalBSON(data []byte) error { + var raw bson.M + if err := bson.Unmarshal(data, &raw); err != nil { + return fmt.Errorf("failed to unmarshal raw data: %w", err) + } + + // Extract driver first + if d, ok := raw["driver"].(string); ok { + cfg.Driver = d + } + + // Nothing more to do if no parameters + paramsRaw, ok := raw["parameters"] + if !ok || paramsRaw == nil { + return nil + } + + // Marshal the inner parameters object for re-decoding + paramBytes, err := bson.Marshal(paramsRaw) + if err != nil { + return fmt.Errorf("failed to re-marshal parameters: %w", err) + } + + switch cfg.Driver { + case "gpu.nvidia.com": + var nvidia nvidia.GPUConfigParametersImpl + if err := bson.Unmarshal(paramBytes, &nvidia); err != nil { + return fmt.Errorf("failed to decode nvidia parameters: %w", err) + } + cfg.Parameters = nvidia + + default: + var generic dra.OpaqueParams + if err := bson.Unmarshal(paramBytes, &generic); err != nil { + return fmt.Errorf("failed to decode generic parameters: %w", err) + } + cfg.Parameters = generic + } + + return nil +} + +type ResourceClaimAllocationResultPublic struct { + Pool string `bson:"pool,omitempty"` + Device string `bson:"device,omitempty"` + Request string `bson:"request,omitempty"` + ShareID string `bson:"shareID,omitempty"` + AdminAccess bool `bson:"adminAccess,omitempty"` +} + +type ResourceClaimConsumerPublic struct { + APIGroup string `bson:"apiGroup,omitempty"` + Resource string `bson:"resource,omitempty"` + Name string `bson:"name,omitempty"` + UID string `bson:"uid,omitempty"` +} + +func (r *ResourceClaimPublic) Created() bool { + return r.CreatedAt != time.Time{} +} + +func (r *ResourceClaimPublic) IsPlaceholder() bool { + return false +} + +func CreateResourceClaimPublicFromRead(claim *resourcev1.ResourceClaim) *ResourceClaimPublic { + if claim == nil { + return nil + } + + rc := &ResourceClaimPublic{ + Name: claim.Name, + Namespace: claim.Namespace, + CreatedAt: formatCreatedAt(claim.Annotations), + } + + rc.DeviceRequests = make([]ResourceClaimDeviceRequestPublic, 0, len(claim.Spec.Devices.Requests)) + for _, req := range claim.Spec.Devices.Requests { + var request ResourceClaimDeviceRequestPublic = ResourceClaimDeviceRequestPublic{ + Name: req.Name, + } + + if req.Exactly != nil { + request.RequestsExactly = &ResourceClaimExactlyRequestPublic{ + ResourceClaimBaseRequestPublic: ResourceClaimBaseRequestPublic{ + AllocationMode: string(req.Exactly.AllocationMode), + CapacityRequests: func() map[resourcev1.QualifiedName]resource.Quantity { + if req.Exactly.Capacity == nil { + return nil + } + return req.Exactly.Capacity.Requests + }(), + Count: req.Exactly.Count, + DeviceClassName: req.Exactly.DeviceClassName, + }, + AdminAccess: req.Exactly.AdminAccess, + } + request.RequestsExactly.SelectorCelExprs = make([]string, 0, len(req.Exactly.Selectors)) + for _, sel := range req.Exactly.Selectors { + if sel.CEL != nil && sel.CEL.Expression != "" { + request.RequestsExactly.SelectorCelExprs = append(request.RequestsExactly.SelectorCelExprs, sel.CEL.Expression) + } + } + } + if len(req.FirstAvailable) > 0 { + request.RequestsFirstAvaliable = make([]ResourceClaimBaseRequestPublic, 0, len(req.FirstAvailable)) + for _, subReq := range req.FirstAvailable { + br := ResourceClaimBaseRequestPublic{ + AllocationMode: string(subReq.AllocationMode), + CapacityRequests: subReq.Capacity.Requests, + Count: subReq.Count, + DeviceClassName: subReq.DeviceClassName, + } + br.SelectorCelExprs = make([]string, 0, len(req.Exactly.Selectors)) + for _, sel := range req.Exactly.Selectors { + if sel.CEL != nil && sel.CEL.Expression != "" { + br.SelectorCelExprs = append(br.SelectorCelExprs, sel.CEL.Expression) + } + } + request.RequestsFirstAvaliable = append(request.RequestsFirstAvaliable, br) + } + } + + rc.DeviceRequests = append(rc.DeviceRequests, request) + } + + for _, cfg := range claim.Spec.Devices.Config { + + if cfg.Opaque != nil && len(cfg.Opaque.Parameters.Raw) > 0 { + + opaqueParams, err := parsers.Parse[dra.OpaqueParams](bytes.NewReader(cfg.Opaque.Parameters.Raw)) + if err != nil { + // TODO: handle/log this error, but just stkip the invalid ones for now + continue + } + + for i, req := range rc.DeviceRequests { + if slices.Contains(cfg.Requests, req.Name) { + rc.DeviceRequests[i].Config = &ResourceClaimOpaquePublic{ + Driver: cfg.Opaque.Driver, + Parameters: opaqueParams, + } + } + } + } + + } + + if claim.Status.Allocation != nil { + rc.Allocated = true + for _, res := range claim.Status.Allocation.Devices.Results { + ar := ResourceClaimAllocationResultPublic{ + Pool: res.Pool, + Device: res.Device, + Request: res.Request, + } + if res.ShareID != nil { + ar.ShareID = string(*res.ShareID) + } + if res.AdminAccess != nil { + ar.AdminAccess = *res.AdminAccess + } + + rc.AllocationResults = append(rc.AllocationResults, ar) + } + + for _, consumer := range claim.Status.ReservedFor { + rc.Consumers = append(rc.Consumers, ResourceClaimConsumerPublic{ + APIGroup: consumer.APIGroup, + Resource: consumer.Resource, + Name: consumer.Name, + UID: string(consumer.UID), + }) + } + } + + return rc +} diff --git a/pkg/subsystems/k8s/models/resourceclaim_status.go b/pkg/subsystems/k8s/models/resourceclaim_status.go new file mode 100644 index 00000000..9da62c80 --- /dev/null +++ b/pkg/subsystems/k8s/models/resourceclaim_status.go @@ -0,0 +1,7 @@ +package models + +type ResourceClaimStatus struct { + Allocated bool `bson:"allocated"` + AllocationResults []ResourceClaimAllocationResultPublic `bson:"allocationResults,omitempty"` + Consumers []ResourceClaimConsumerPublic `bson:"consumers,omitempty"` +} diff --git a/pkg/subsystems/k8s/parsers/dra/nvidia/gpuConfig.go b/pkg/subsystems/k8s/parsers/dra/nvidia/gpuConfig.go new file mode 100644 index 00000000..d98c0182 --- /dev/null +++ b/pkg/subsystems/k8s/parsers/dra/nvidia/gpuConfig.go @@ -0,0 +1,71 @@ +package nvidia + +import ( + "encoding/json" + "io" + "strings" + + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/api/nvidia" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra" +) + +const ( + supportedApiVersion = "resource.nvidia.com/v1beta1" + supportedKind = "GpuConfig" +) + +type GPUConfigParametersImpl struct { + nvidia.GpuConfig +} + +func (s GPUConfigParametersImpl) MetaAPIVersion() string { + return s.APIVersion +} + +func (s GPUConfigParametersImpl) MetaKind() string { + return s.Kind +} + +type GPUConfigParametersParserImpl struct { +} + +func (GPUConfigParametersParserImpl) CanParse(raw io.Reader) bool { + decoder := json.NewDecoder(raw) + var tmp map[string]any + if err := decoder.Decode(&tmp); err != nil { + return false + } + + // Helper to find a key ignoring case + findKey := func(m map[string]any, key string) (any, bool) { + for k, v := range m { + if strings.EqualFold(k, key) { + return v, true + } + } + return nil, false + } + + // Get apiVersion ignoring key case + if apiVersionRaw, ok := findKey(tmp, "apiVersion"); ok { + if apiVersion, ok := apiVersionRaw.(string); ok && strings.EqualFold(apiVersion, supportedApiVersion) { + if kindRaw, ok := findKey(tmp, "kind"); ok { + if kind, ok := kindRaw.(string); ok && strings.EqualFold(kind, supportedKind) { + return true + } + } + } + } + + return false +} + +func (GPUConfigParametersParserImpl) Parse(raw io.Reader) (dra.OpaqueParams, error) { + var gpuCfg nvidia.GpuConfig + decoder := json.NewDecoder(raw) + if err := decoder.Decode(&gpuCfg); err != nil { + return nil, err + } + + return GPUConfigParametersImpl{GpuConfig: gpuCfg}, nil +} diff --git a/pkg/subsystems/k8s/parsers/dra/nvidia/gpuConfig_test.go b/pkg/subsystems/k8s/parsers/dra/nvidia/gpuConfig_test.go new file mode 100644 index 00000000..8ee4d6a2 --- /dev/null +++ b/pkg/subsystems/k8s/parsers/dra/nvidia/gpuConfig_test.go @@ -0,0 +1,57 @@ +package nvidia_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra/nvidia" +) + +func TestGPUConfigParser(t *testing.T) { + data := []byte(`{ + "apiVersion":"resource.nvidia.com/v1beta1", + "kind":"GpuConfig", + "sharing":{} + }`) + + parser := nvidia.GPUConfigParametersParserImpl{} + + if !parser.CanParse(bytes.NewReader(data)) { + t.Fatal("CanParse returned false for valid GPUConfig JSON") + } + + out, err := parser.Parse(bytes.NewReader(data)) + if err != nil { + t.Fatalf("Parse returned error: %v", err) + } + + if !strings.HasPrefix(out.MetaAPIVersion(), "resource.nvidia.com/") { + t.Errorf("unexpected APIVersion: got %s", out.MetaAPIVersion()) + } + if out.MetaKind() != "GpuConfig" { + t.Errorf("unexpected Kind: got %s", out.MetaKind()) + } +} + +func TestGPUConfigParser2(t *testing.T) { + data := []byte(`{"apiVersion":"resource.nvidia.com/v1beta1","kind":"GpuConfig","sharing":{"mpsConfig":{"defaultActiveThreadPercentage":50,"defaultPinnedDeviceMemoryLimit":"10Gi"},"strategy":"MPS"}}`) + + parser := nvidia.GPUConfigParametersParserImpl{} + + if !parser.CanParse(bytes.NewReader(data)) { + t.Fatal("CanParse returned false for valid GPUConfig JSON") + } + + out, err := parser.Parse(bytes.NewReader(data)) + if err != nil { + t.Fatalf("Parse returned error: %v", err) + } + + if !strings.HasPrefix(out.MetaAPIVersion(), "resource.nvidia.com/") { + t.Errorf("unexpected APIVersion: got %s", out.MetaAPIVersion()) + } + if out.MetaKind() != "GpuConfig" { + t.Errorf("unexpected Kind: got %s", out.MetaKind()) + } +} diff --git a/pkg/subsystems/k8s/parsers/dra/parameters.go b/pkg/subsystems/k8s/parsers/dra/parameters.go new file mode 100644 index 00000000..316cd454 --- /dev/null +++ b/pkg/subsystems/k8s/parsers/dra/parameters.go @@ -0,0 +1,13 @@ +package dra + +import "io" + +type OpaqueParams interface { + MetaAPIVersion() string + MetaKind() string +} + +type ParametersParser interface { + CanParse(raw io.Reader) bool + Parse(raw io.Reader) (OpaqueParams, error) +} diff --git a/pkg/subsystems/k8s/parsers/errors.go b/pkg/subsystems/k8s/parsers/errors.go new file mode 100644 index 00000000..80cbd44e --- /dev/null +++ b/pkg/subsystems/k8s/parsers/errors.go @@ -0,0 +1,8 @@ +package parsers + +import "errors" + +var ( + ErrParserNotFound = errors.New("no parser found") + ErrParserCastFailed = errors.New("parsed value cannot be cast to T") +) diff --git a/pkg/subsystems/k8s/parsers/factory.go b/pkg/subsystems/k8s/parsers/factory.go new file mode 100644 index 00000000..ddafc188 --- /dev/null +++ b/pkg/subsystems/k8s/parsers/factory.go @@ -0,0 +1,75 @@ +package parsers + +import ( + "bytes" + "io" + + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra/nvidia" +) + +var ( + // If we add other vendors that support DRA and have driver specific + // configuration, we can add it here and just implement the + // dra.ParamerersParser interface. + // + // Currently only NVIDIA has opaque driver parameters, + // This is used to configure things like Sharing configuration. + // Like decide what sharing strategy should be used, + // and also provide configurations for that configuration. + GpuOpaqueParamParsers = []dra.ParametersParser{ + + nvidia.GPUConfigParametersParserImpl{}, + } +) + +// Parse attempts to parse the provided raw data into the desired type T. +// It iterates through all registered vendor-specific parsers and uses CanParse(). +// The first parser that returns true will be used for parsing. +// +// The type T must implement dra.OpaqueParams; otherwise, the function returns +// the zero value of T. +// +// Returns: +// - Parsed value of type T if a suitable parser is found and parsing succeeds. +// - ErrParserNotFound if no parser claims the data. +// - ErrParserCastFailed if the parser returns a value that cannot be cast to T. +// - Any error returned by the underlying parser. +// +// The raw io.Reader will be automatically rewound between CanParse and Parse, +// so you can safely pass a bytes.Reader or similar. +func Parse[T any](raw io.Reader) (T, error) { + var zero T + + // Wrap raw in a bytes.Buffer if it's not already a ReadSeeker + var buf bytes.Buffer + if _, err := io.Copy(&buf, raw); err != nil { + return zero, err + } + reader := bytes.NewReader(buf.Bytes()) + + for _, vendor := range GpuOpaqueParamParsers { + // Rewind reader before CanParse + if _, err := reader.Seek(0, io.SeekStart); err != nil { + return zero, err + } + + if vendor.CanParse(reader) { + // Rewind again before Parse + if _, err := reader.Seek(0, io.SeekStart); err != nil { + return zero, err + } + + parsed, err := vendor.Parse(reader) + if err != nil { + return zero, err + } + + if casted, ok := parsed.(T); ok { + return casted, nil + } + return zero, ErrParserCastFailed + } + } + return zero, ErrParserNotFound +} diff --git a/pkg/subsystems/k8s/parsers/factory_test.go b/pkg/subsystems/k8s/parsers/factory_test.go new file mode 100644 index 00000000..5caa74e2 --- /dev/null +++ b/pkg/subsystems/k8s/parsers/factory_test.go @@ -0,0 +1,119 @@ +package parsers_test + +import ( + "bytes" + "errors" + "io" + "testing" + + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra" +) + +type mockParser struct { + canParseResult bool + parseResult dra.OpaqueParams + parseErr error +} + +func (m mockParser) CanParse(r io.Reader) bool { + return m.canParseResult +} + +func (m mockParser) Parse(r io.Reader) (dra.OpaqueParams, error) { + return m.parseResult, m.parseErr +} + +type mockParams struct { + Name string + APIVersion string + Kind string +} + +func (m mockParams) MetaAPIVersion() string { + return m.APIVersion +} + +func (m mockParams) MetaKind() string { + return m.Kind +} + +func (m mockParams) Opaque() {} + +func TestParse_Success(t *testing.T) { + // Override global parser slice for testing + original := parsers.GpuOpaqueParamParsers + defer func() { parsers.GpuOpaqueParamParsers = original }() + + expected := mockParams{Name: "test", APIVersion: "resource.test.kth.se/v1", Kind: "Mock"} + parsers.GpuOpaqueParamParsers = []dra.ParametersParser{ + mockParser{ + canParseResult: true, + parseResult: expected, + parseErr: nil, + }, + } + + data := bytes.NewReader([]byte("some raw data")) + result, err := parsers.Parse[mockParams](data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Name != expected.Name { + t.Errorf("expected %v, got %v", expected, result) + } +} + +func TestParse_NoParserFound(t *testing.T) { + original := parsers.GpuOpaqueParamParsers + defer func() { parsers.GpuOpaqueParamParsers = original }() + + parsers.GpuOpaqueParamParsers = []dra.ParametersParser{ + mockParser{canParseResult: false}, + } + + data := bytes.NewReader([]byte("data")) + _, err := parsers.Parse[mockParams](data) + if !errors.Is(err, parsers.ErrParserNotFound) { + t.Errorf("expected ErrParserNotFound, got %v", err) + } +} + +func TestParse_CastFailed(t *testing.T) { + original := parsers.GpuOpaqueParamParsers + defer func() { parsers.GpuOpaqueParamParsers = original }() + + parsers.GpuOpaqueParamParsers = []dra.ParametersParser{ + mockParser{ + canParseResult: true, + parseResult: nil, + parseErr: nil, + }, + } + + data := bytes.NewReader([]byte("data")) + _, err := parsers.Parse[mockParams](data) + if !errors.Is(err, parsers.ErrParserCastFailed) { + t.Errorf("expected ErrParserCastFailed, got %v", err) + } +} + +func TestParse_ParseError(t *testing.T) { + original := parsers.GpuOpaqueParamParsers + defer func() { parsers.GpuOpaqueParamParsers = original }() + + expectedErr := errors.New("parse failed") + parsers.GpuOpaqueParamParsers = []dra.ParametersParser{ + mockParser{ + canParseResult: true, + parseResult: nil, + parseErr: expectedErr, + }, + } + + data := bytes.NewReader([]byte("data")) + _, err := parsers.Parse[mockParams](data) + if err != expectedErr { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} diff --git a/pkg/subsystems/k8s/parsers/parser.go b/pkg/subsystems/k8s/parsers/parser.go new file mode 100644 index 00000000..9167a8c3 --- /dev/null +++ b/pkg/subsystems/k8s/parsers/parser.go @@ -0,0 +1,9 @@ +package parsers + +import "io" + +// Parser is a generic interface for parsers +type Parser[T any] interface { + CanParse(raw io.Reader) bool + Parse(raw io.Reader, out T) error +} diff --git a/pkg/subsystems/k8s/resourceclaim.go b/pkg/subsystems/k8s/resourceclaim.go new file mode 100644 index 00000000..8f9405db --- /dev/null +++ b/pkg/subsystems/k8s/resourceclaim.go @@ -0,0 +1,175 @@ +package k8s + +import ( + "context" + "fmt" + "time" + + "github.com/kthcloud/go-deploy/pkg/log" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/models" + k8sModels "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/models" + resourcev1 "k8s.io/api/resource/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/informers" + "k8s.io/client-go/tools/cache" +) + +// ReadResourceClaim reads a ResourceClaim from Kubernetes. +func (client *Client) ReadResourceClaim(name string) (*models.ResourceClaimPublic, error) { + makeError := func(err error) error { + return fmt.Errorf("failed to read k8s ResourceClaim %s. details: %w", name, err) + } + + if name == "" { + log.Println("No name supplied when reading k8s ResourceClaim. Assuming it was deleted") + return nil, nil + } + + res, err := client.K8sClient.ResourceV1().ResourceClaims(client.Namespace).Get(context.TODO(), name, v1.GetOptions{}) + if err != nil { + if IsNotFoundErr(err) { + return nil, nil + } + + return nil, makeError(err) + } + + return models.CreateResourceClaimPublicFromRead(res), nil +} + +// CreateResourceClaim creates a ResourceClaim in Kubernetes. +func (client *Client) CreateResourceClaim(public *models.ResourceClaimPublic) (*models.ResourceClaimPublic, error) { + makeError := func(err error) error { + return fmt.Errorf("failed to create k8s ResourceClaim %s. details: %w", public.Name, err) + } + + if public.Name == "" { + log.Println("No name supplied when creating k8s ResourceClaim. Assuming it was deleted") + return nil, nil + } + + rc, err := client.K8sClient.ResourceV1().ResourceClaims(public.Namespace).Get(context.TODO(), public.Name, v1.GetOptions{}) + if err != nil && !IsNotFoundErr(err) { + return nil, makeError(err) + } + + if err == nil { + return models.CreateResourceClaimPublicFromRead(rc), nil + } + + public.CreatedAt = time.Now() + + manifest := CreateResourceClaimManifest(public) + res, err := client.K8sClient.ResourceV1().ResourceClaims(public.Namespace).Create(context.TODO(), manifest, v1.CreateOptions{}) + if err != nil { + return nil, makeError(err) + } + + return models.CreateResourceClaimPublicFromRead(res), nil +} + +// DeleteResourceClaim deletes a ResourceClaim in Kubernetes. +func (client *Client) DeleteResourceClaim(name string) error { + makeError := func(err error) error { + return fmt.Errorf("failed to delete k8s ResourceClaim %s. details: %w", name, err) + } + + if name == "" { + log.Println("No name supplied when deleting k8s ResourceClaim. Assuming it was deleted") + return nil + } + + err := client.K8sClient.ResourceV1().ResourceClaims(client.Namespace).Delete(context.TODO(), name, v1.DeleteOptions{}) + if err != nil && !IsNotFoundErr(err) { + return makeError(err) + } + + err = client.waitResourceClaimDeleted(name) + if err != nil { + return makeError(err) + } + + return nil +} + +// waitResourceClaimDeleted waits for a ResourceClaim to be deleted. +func (client *Client) waitResourceClaimDeleted(name string) error { + maxWait := 120 + for range maxWait { + time.Sleep(1 * time.Second) + _, err := client.K8sClient.ResourceV1().ResourceClaims(client.Namespace).Get(context.TODO(), name, v1.GetOptions{}) + if err != nil && IsNotFoundErr(err) { + return nil + } + } + + return fmt.Errorf("timeout waiting for ResourceClaim %s to be deleted", name) +} + +// SetupResourceClaimWatcher is a function that sets up a resourceClaim watcher with a callback. +// It triggers the callback when a resourceClaim event occurs. +func (client *Client) SetupResourceClaimWatcher(ctx context.Context, callback func(resourceClaimName string, status k8sModels.ResourceClaimStatus, event string)) error { + factory := informers.NewSharedInformerFactoryWithOptions(client.K8sClient, 0, informers.WithNamespace(client.Namespace)) + resoureClaimInformer := factory.Resource().V1().ResourceClaims().Informer() + + allowedResourceClaim := func(_ *resourcev1.ResourceClaim) bool { + return true + } + + _, err := resoureClaimInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj any) { + claim, ok := obj.(*resourcev1.ResourceClaim) + if !ok { + return + } + + if !allowedResourceClaim(claim) { + return + } + + callback(claim.Name, rcToK8sModel(claim), PodEventAdded) + }, + UpdateFunc: func(oldObj, newObj any) { + claim, ok := newObj.(*resourcev1.ResourceClaim) + if !ok { + return + } + + if !allowedResourceClaim(claim) { + return + } + + callback(claim.Name, rcToK8sModel(claim), PodEventUpdated) + }, + DeleteFunc: func(obj any) { + claim, ok := obj.(*resourcev1.ResourceClaim) + if !ok { + return + } + + if !allowedResourceClaim(claim) { + return + } + + callback(claim.Name, rcToK8sModel(claim), PodEventDeleted) + }, + }) + if err != nil { + return err + } + + factory.Start(ctx.Done()) + factory.WaitForCacheSync(ctx.Done()) + + return nil +} + +func rcToK8sModel(rc *resourcev1.ResourceClaim) k8sModels.ResourceClaimStatus { + rcp := k8sModels.CreateResourceClaimPublicFromRead(rc) + + return k8sModels.ResourceClaimStatus{ + Allocated: rcp.Allocated, + AllocationResults: rcp.AllocationResults, + Consumers: rcp.Consumers, + } +} diff --git a/routers/api/v2/deployment.go b/routers/api/v2/deployment.go index debff7b1..a4547d97 100644 --- a/routers/api/v2/deployment.go +++ b/routers/api/v2/deployment.go @@ -2,6 +2,9 @@ package v2 import ( "errors" + "fmt" + "maps" + "slices" "strconv" "strings" @@ -16,8 +19,11 @@ import ( "github.com/kthcloud/go-deploy/pkg/config" "github.com/kthcloud/go-deploy/pkg/sys" "github.com/kthcloud/go-deploy/service" + "github.com/kthcloud/go-deploy/service/clients" + "github.com/kthcloud/go-deploy/service/core" sErrors "github.com/kthcloud/go-deploy/service/errors" "github.com/kthcloud/go-deploy/service/v2/deployments/opts" + gpuClaimOpts "github.com/kthcloud/go-deploy/service/v2/gpu_claims/opts" teamOpts "github.com/kthcloud/go-deploy/service/v2/teams/opts" v12 "github.com/kthcloud/go-deploy/service/v2/utils" ) @@ -26,7 +32,7 @@ import ( // @Summary Get deployment // @Description Get deployment // @Tags Deployment -// @Produce json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param deploymentId path string true "Deployment ID" @@ -82,7 +88,7 @@ func GetDeployment(c *gin.Context) { // @Summary List deployments // @Description List deployments // @Tags Deployment -// @Produce json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param all query bool false "List all" @@ -146,8 +152,8 @@ func ListDeployments(c *gin.Context) { // @Summary Create deployment // @Description Create deployment // @Tags Deployment -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param body body body.DeploymentCreate true "Deployment body" @@ -191,6 +197,17 @@ func CreateDeployment(c *gin.Context) { return } + for _, gpu := range requestBody.GPUs { + if strings.TrimSpace(gpu.ClaimName) == "" || strings.TrimSpace(gpu.Name) == "" { + context.UserError("Invalid gpu claim reference, requires both ClaimName and Name") + return + } + } + + if requestBody.Zone == nil { + requestBody.Zone = &config.Config.Deployment.DefaultZone + } + if requestBody.Zone != nil { zone := deployV2.System().GetZone(*requestBody.Zone) if zone == nil { @@ -207,6 +224,13 @@ func CreateDeployment(c *gin.Context) { context.Forbidden("Zone does not have deployment capability") return } + + if len(requestBody.GPUs) > 0 { + if !deployV2.System().ZoneHasCapability(*requestBody.Zone, configModels.ZoneCapabilityDRA) { + context.Forbidden("Zone does not have dra capability") + return + } + } } if requestBody.CustomDomain != nil && !effectiveRole.Permissions.UseCustomDomains { @@ -219,6 +243,19 @@ func CreateDeployment(c *gin.Context) { return } + if err := validateGpuRequests(&requestBody.GPUs, *requestBody.Zone, auth, deployV2); err != nil { + if errors.Is(err, ErrCouldNotGetGpuClaims) { + context.ServerError(err, ErrCouldNotGetGpuClaims) + return + } + if errors.Is(err, sErrors.NewZoneCapabilityMissingError(*requestBody.Zone, configModels.ZoneCapabilityDRA)) { + context.Forbidden("Zone lacks DRA capability") + return + } + context.UserError(err.Error()) + return + } + err = deployV2.Deployments().CheckQuota("", &opts.QuotaOptions{Create: &requestBody}) if err != nil { var quotaExceededErr sErrors.QuotaExceededError @@ -239,7 +276,6 @@ func CreateDeployment(c *gin.Context) { "params": requestBody, "authInfo": auth, }) - if err != nil { context.ServerError(err, ErrInternal) return @@ -255,8 +291,8 @@ func CreateDeployment(c *gin.Context) { // @Summary Delete deployment // @Description Delete deployment // @Tags Deployment -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param deploymentId path string true "Deployment ID" @@ -338,8 +374,8 @@ func DeleteDeployment(c *gin.Context) { // @Summary Update deployment // @Description Update deployment // @Tags Deployment -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param deploymentId path string true "Deployment ID" @@ -396,6 +432,19 @@ func UpdateDeployment(c *gin.Context) { } } + if err := validateGpuRequests(requestBody.GPUs, deployment.Zone, auth, deployV2); err != nil { + if errors.Is(err, ErrCouldNotGetGpuClaims) { + context.ServerError(err, ErrCouldNotGetGpuClaims) + return + } + if errors.Is(err, sErrors.NewZoneCapabilityMissingError(deployment.Zone, configModels.ZoneCapabilityDRA)) { + context.Forbidden("Zone lacks DRA capability") + return + } + context.UserError(err.Error()) + return + } + if requestBody.NeverStale != nil && !auth.User.IsAdmin { context.Forbidden("User is not allowed to modify the neverStale value") return @@ -425,7 +474,6 @@ func UpdateDeployment(c *gin.Context) { "params": requestBody, "authInfo": auth, }) - if err != nil { context.ServerError(err, ErrInternal) return @@ -437,6 +485,87 @@ func UpdateDeployment(c *gin.Context) { }) } +func validateGpuRequests(gpus *[]body.DeploymentGPU, zone string, auth *core.AuthInfo, deployV2 clients.V2) error { + if gpus != nil { + if len(*gpus) > 0 { + + if !deployV2.System().ZoneHasCapability(zone, configModels.ZoneCapabilityDRA) { + return sErrors.NewZoneCapabilityMissingError(zone, configModels.ZoneCapabilityDRA) + } + + roles := make([]string, 0, 2) + if role := auth.GetEffectiveRole(); role != nil { + roles = append(roles, role.Name) + } + if auth.User != nil && auth.User.IsAdmin { + roles = append(roles, "admin") + } + claimReqMap := make(map[string][]string, len(*gpus)) + for _, gpu := range *gpus { + if reqs, found := claimReqMap[gpu.ClaimName]; found { + claimReqMap[gpu.ClaimName] = append(reqs, gpu.Name) + } else { + claimReqMap[gpu.ClaimName] = []string{gpu.Name} + } + } + names := slices.AppendSeq(make([]string, 0, len(claimReqMap)), maps.Keys(claimReqMap)) + claims, err := deployV2.GpuClaims().List(gpuClaimOpts.ListOpts{ + Names: &names, + Zone: &zone, + Roles: &roles, + }) + if err != nil { + return errors.Join(err, ErrCouldNotGetGpuClaims) + } + availableClaimReqMap := make(map[string][]string, len(claims)) + for _, claim := range claims { + if reqs, found := availableClaimReqMap[claim.Name]; found { + availableClaimReqMap[claim.Name] = append(reqs, slices.AppendSeq(make([]string, 0, len(claim.Requested)), maps.Keys(claim.Requested))...) + } else { + availableClaimReqMap[claim.Name] = slices.AppendSeq(make([]string, 0, len(claim.Requested)), maps.Keys(claim.Requested)) + } + } + missingClaims := make([]string, 0) + missingRequests := make([]string, 0) + for claimName, reqsName := range claimReqMap { + if availableReqs, found := availableClaimReqMap[claimName]; found { + for _, req := range reqsName { + if !slices.Contains(availableReqs, req) { + missingRequests = append(missingRequests, + fmt.Sprintf("%s:%s", claimName, req), + ) + } + } + } else { + missingClaims = append(missingClaims, claimName) + } + } + var errs []error + + if len(missingClaims) > 0 { + for _, c := range missingClaims { + errs = append(errs, + fmt.Errorf("missing GPU claim: %s", c), + ) + } + } + + if len(missingRequests) > 0 { + for _, r := range missingRequests { + errs = append(errs, + fmt.Errorf("missing GPU request in claim: %s", r), + ) + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + } + } + return nil +} + func getDeploymentExternalPort(zoneName string) *int { zone := config.Config.GetZone(zoneName) if zone == nil { diff --git a/routers/api/v2/deployment_command.go b/routers/api/v2/deployment_command.go index 29d2b911..461012f5 100644 --- a/routers/api/v2/deployment_command.go +++ b/routers/api/v2/deployment_command.go @@ -13,8 +13,8 @@ import ( // @Summary Do command // @Description Do command // @Tags Deployment -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param deploymentId path string true "Deployment ID" diff --git a/routers/api/v2/deployment_hook.go b/routers/api/v2/deployment_hook.go index d73bcb7b..e98e5d71 100644 --- a/routers/api/v2/deployment_hook.go +++ b/routers/api/v2/deployment_hook.go @@ -20,10 +20,10 @@ import ( // @Summary Handle Harbor hook // @Description Handle Harbor hook // @Tags Deployment -// @Accept json +// @Accept json // @Param Authorization header string false "Basic auth token" // @Param body body body.HarborWebhook true "Harbor webhook body" -// @Produce json +// @Produce json // @Success 204 "No Content" // @Failure 400 {object} sys.ErrorResponse // @Failure 401 {object} sys.ErrorResponse @@ -34,7 +34,6 @@ func HandleHarborHook(c *gin.Context) { context := sys.NewContext(c) token, err := getHarborTokenFromAuthHeader(context) - if err != nil { context.ServerError(err, ErrInternal) return diff --git a/routers/api/v2/deployment_logs.go b/routers/api/v2/deployment_logs.go index 601b8163..73efc861 100644 --- a/routers/api/v2/deployment_logs.go +++ b/routers/api/v2/deployment_logs.go @@ -19,7 +19,7 @@ import ( // @Summary Get logs using Server-Sent Events // @Description Get logs using Server-Sent Events // @Tags Deployment -// @Produce json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param deploymentId path string true "Deployment ID" @@ -92,4 +92,4 @@ func GetLogs(c *gin.Context) { }) } -//mhuaaaaaaaaaaaah, i love you i love you i love you +// mhuaaaaaaaaaaaah, i love you i love you i love you diff --git a/routers/api/v2/discover.go b/routers/api/v2/discover.go index cf000b72..7f020072 100644 --- a/routers/api/v2/discover.go +++ b/routers/api/v2/discover.go @@ -10,7 +10,7 @@ import ( // @Summary Discover // @Description Discover // @Tags Discover -// @Produce json +// @Produce json // @Success 200 {object} body.DiscoverRead // @Failure 500 {object} sys.ErrorResponse // @Router /v2/discover [get] diff --git a/routers/api/v2/errors.go b/routers/api/v2/errors.go index 584b8e51..cabab0f8 100644 --- a/routers/api/v2/errors.go +++ b/routers/api/v2/errors.go @@ -25,6 +25,8 @@ var ( // ErrVmNotReady is returned when the VM is not ready. // This is normally caused by the internal CloudStack VM not being created yet ErrVmNotReady = fmt.Errorf("VM not ready") + // ErrCouldNotGetGpuClaims is returned when getting gpu claims fails + ErrCouldNotGetGpuClaims = fmt.Errorf("could not get gpu claims") ) // MakeVmToLargeForHostErr creates a ErrVmToLargeForHost with the available CPU and RAM diff --git a/routers/api/v2/gpu_claim.go b/routers/api/v2/gpu_claim.go new file mode 100644 index 00000000..491b7e02 --- /dev/null +++ b/routers/api/v2/gpu_claim.go @@ -0,0 +1,310 @@ +package v2 + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/kthcloud/go-deploy/dto/v2/body" + "github.com/kthcloud/go-deploy/dto/v2/query" + "github.com/kthcloud/go-deploy/dto/v2/uri" + configModels "github.com/kthcloud/go-deploy/models/config" + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/models/version" + "github.com/kthcloud/go-deploy/pkg/sys" + "github.com/kthcloud/go-deploy/service" + "github.com/kthcloud/go-deploy/service/v2/gpu_claims/opts" + "github.com/kthcloud/go-deploy/service/v2/utils" +) + +// GetGpuClaim +// @Summary Get GPU claim +// @Description Get GPU claim +// @Tags GpuClaim +// @Produce json +// @Security ApiKeyAuth +// @Security KeycloakOAuth +// @Param gpuClaimId path string true "GPU claim ID" +// @Success 200 {object} body.GpuClaimRead +// @Failure 400 {object} sys.ErrorResponse +// @Failure 404 {object} sys.ErrorResponse +// @Failure 423 {object} sys.ErrorResponse +// @Failure 500 {object} sys.ErrorResponse +// @Router /v2/gpuClaims/{gpuClaimId} [get] +func GetGpuClaim(c *gin.Context) { + context := sys.NewContext(c) + + var requestURI uri.GpuClaimGet + if err := context.GinContext.ShouldBindUri(&requestURI); err != nil { + context.BindingError(CreateBindingError(err)) + return + } + + auth, err := WithAuth(&context) + if err != nil { + context.ServerError(err, ErrAuthInfoNotAvailable) + return + } + + if auth.User == nil || !auth.User.IsAdmin { + context.Forbidden("User is not allowed to get GpuClaim") + return + } + + deployV2 := service.V2(auth) + + GpuClaim, err := deployV2.GpuClaims().Get(requestURI.GpuClaimID) + if err != nil { + context.ServerError(err, ErrInternal) + return + } + + if GpuClaim == nil { + context.NotFound("GPU claim not found") + return + } + + context.Ok(GpuClaim.ToDTO()) +} + +// ListGpuClaims +// @Summary List GPU claims +// @Description List GPU claims +// @Tags GpuClaim +// @Produce json +// @Security ApiKeyAuth +// @Security KeycloakOAuth +// @Param page query int false "Page number" +// @Param pageSize query int false "Number of items per page" +// @Param detailed query bool false "Admin detailed list" +// @Success 200 {array} body.GpuClaimRead +// @Failure 400 {object} sys.ErrorResponse +// @Failure 404 {object} sys.ErrorResponse +// @Failure 423 {object} sys.ErrorResponse +// @Failure 500 {object} sys.ErrorResponse +// @Router /v2/gpuClaims [get] +func ListGpuClaims(c *gin.Context) { + context := sys.NewContext(c) + + var requestQuery query.GpuClaimList + if err := context.GinContext.ShouldBind(&requestQuery); err != nil { + context.BindingError(CreateBindingError(err)) + return + } + + var requestURI uri.GpuClaimList + if err := context.GinContext.ShouldBindUri(&requestURI); err != nil { + context.BindingError(CreateBindingError(err)) + return + } + + auth, err := WithAuth(&context) + if err != nil { + context.ServerError(err, ErrAuthInfoNotAvailable) + return + } + + if requestQuery.Detailed && (auth.User == nil || !auth.User.IsAdmin) { + context.ErrorResponse(http.StatusForbidden, 403, "only admins can access detailed view of gpu claims") + } + + roles := make([]string, 0, 2) + if role := auth.GetEffectiveRole(); role != nil { + roles = append(roles, role.Name) + } + if auth.User.IsAdmin { + roles = append(roles, "admin") + } + + deployV2 := service.V2(auth) + + GpuClaims, err := deployV2.GpuClaims().List(opts.ListOpts{ + Pagination: utils.GetOrDefaultPagination(requestQuery.Pagination), + Roles: &roles, + }) + if err != nil { + context.ServerError(err, ErrInternal) + return + } + + if GpuClaims == nil { + context.Ok([]any{}) + return + } + + dtoGpuClaims := make([]body.GpuClaimRead, len(GpuClaims)) + for i, GpuClaim := range GpuClaims { + if requestQuery.Detailed { + dtoGpuClaims[i] = GpuClaim.ToDTO() + } else { + dtoGpuClaims[i] = GpuClaim.ToBriefDTO() + } + } + + context.Ok(dtoGpuClaims) +} + +// CreateGpuClaim +// @Summary Create GpuClaim +// @Description Create GpuClaim +// @Tags GpuClaim +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Security KeycloakOAuth +// @Param body body body.GpuClaimCreate true "GpuClaim body" +// @Success 200 {object} body.GpuClaimCreated +// @Failure 400 {object} sys.ErrorResponse +// @Failure 404 {object} sys.ErrorResponse +// @Failure 403 {object} sys.ErrorResponse +// @Failure 500 {object} sys.ErrorResponse +// @Router /v2/gpuClaims [post] +func CreateGpuClaim(c *gin.Context) { + context := sys.NewContext(c) + + var requestBody body.GpuClaimCreate + if err := context.GinContext.ShouldBindJSON(&requestBody); err != nil { + context.BindingError(CreateBindingError(err)) + return + } + + auth, err := WithAuth(&context) + if err != nil { + context.ServerError(err, ErrAuthInfoNotAvailable) + return + } + + if auth.User == nil || !auth.User.IsAdmin { + context.Forbidden("User is not allowed to create GpuClaim") + return + } + + deployV2 := service.V2(auth) + + /*doesNotAlreadyExists, err := deployV2.GpuClaims().NameAvailable(requestBody.Name) + if err != nil { + context.ServerError(err, ErrInternal) + return + } + + if !doesNotAlreadyExists { + context.UserError("GpuClaim already exists") + return + }*/ + + if requestBody.Zone != nil { + zone := deployV2.System().GetZone(*requestBody.Zone) + if zone == nil { + context.NotFound("Zone not found") + return + } + + if !zone.Enabled { + context.Forbidden("Zone is disabled") + return + } + + if !deployV2.System().ZoneHasCapability(*requestBody.Zone, configModels.ZoneCapabilityDRA) { + context.Forbidden("Zone does not have dra capability") + return + } + } + + GpuClaimID := uuid.New().String() + jobID := uuid.New().String() + err = deployV2.Jobs().Create(jobID, auth.User.ID, model.JobCreateGpuClaim, version.V2, map[string]any{ + "id": GpuClaimID, + "params": requestBody, + "authInfo": auth, + }) + if err != nil { + context.ServerError(err, ErrInternal) + return + } + + context.Ok(body.GpuClaimCreated{ + ID: GpuClaimID, + JobID: jobID, + }) +} + +// DeleteGpuClaim +// @Summary Delete GpuClaim +// @Description Delete GpuClaim +// @Tags GpuClaim +// @Produce json +// @Security ApiKeyAuth +// @Security KeycloakOAuth +// @Param gpuClaimId path string true "GpuClaim ID" +// @Success 200 {object} body.GpuClaimCreated +// @Failure 400 {object} sys.ErrorResponse +// @Failure 401 {object} sys.ErrorResponse +// @Failure 404 {object} sys.ErrorResponse +// @Failure 403 {object} sys.ErrorResponse +// @Failure 500 {object} sys.ErrorResponse +// @Router /v2/gpuClaims/{gpuClaimId} [delete] +func DeleteGpuClaim(c *gin.Context) { + context := sys.NewContext(c) + + var requestURI uri.GpuClaimDelete + if err := context.GinContext.ShouldBindUri(&requestURI); err != nil { + context.BindingError(CreateBindingError(err)) + return + } + + auth, err := WithAuth(&context) + if err != nil { + context.ServerError(err, ErrAuthInfoNotAvailable) + return + } + + if auth.User == nil || !auth.User.IsAdmin { + context.Forbidden("GpuClaims can only be deleted by admins") + return + } + + deployV2 := service.V2(auth) + + currentGpuClaim, err := deployV2.GpuClaims().Get(requestURI.GpuClaimID) + if err != nil { + context.ServerError(err, ErrInternal) + return + } + + if currentGpuClaim == nil { + context.NotFound("GpuClaim not found") + return + } + + /*err = deployV2.GpuClaims().StartActivity(requestURI.GpuClaimID, model.ActivityBeingDeleted) + if err != nil { + var failedToStartActivityErr sErrors.FailedToStartActivityError + if errors.As(err, &failedToStartActivityErr) { + context.Locked(failedToStartActivityErr.Error()) + return + } + + if errors.Is(err, sErrors.ErrResourceNotFound) { + context.NotFound("GpuClaim not found") + return + } + + context.ServerError(err, ErrInternal) + return + }*/ + + jobID := uuid.NewString() + err = deployV2.Jobs().Create(jobID, auth.User.ID, model.JobDeleteGpuClaim, version.V2, map[string]any{ + "id": currentGpuClaim.ID, + "authInfo": auth, + }) + if err != nil { + context.ServerError(err, ErrInternal) + return + } + + context.Ok(body.GpuClaimCreated{ + ID: currentGpuClaim.ID, + JobID: jobID, + }) +} diff --git a/routers/api/v2/hosts.go b/routers/api/v2/hosts.go index 97b38a1d..7b302065 100644 --- a/routers/api/v2/hosts.go +++ b/routers/api/v2/hosts.go @@ -13,7 +13,7 @@ import ( // @Summary List Hosts // @Description List Hosts // @Tags Host -// @Produce json +// @Produce json // @Success 200 {array} body.HostRead // @Failure 500 {object} sys.ErrorResponse // @Router /v2/hosts [get] @@ -35,7 +35,7 @@ func ListHosts(c *gin.Context) { // @Summary List Hosts verbose // @Description List Hosts verbose // @Tags Host -// @Produce json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Success 200 {array} body.HostVerboseRead diff --git a/routers/api/v2/job.go b/routers/api/v2/job.go index 52ad96bb..bd84b6d4 100644 --- a/routers/api/v2/job.go +++ b/routers/api/v2/job.go @@ -20,8 +20,8 @@ import ( // @Summary GetJob job by id // @Description GetJob job by id // @Tags Job -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param jobId path string true "Job ID" @@ -64,8 +64,8 @@ func GetJob(c *gin.Context) { // @Summary List jobs // @Description List jobs // @Tags Job -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param all query bool false "List all" @@ -123,8 +123,8 @@ func ListJobs(c *gin.Context) { // @Summary Update job // @Description Update job. Only allowed for admins. // @Tags Job -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param jobId path string true "Job ID" diff --git a/routers/api/v2/notification.go b/routers/api/v2/notification.go index 27f2fc03..f02182c9 100644 --- a/routers/api/v2/notification.go +++ b/routers/api/v2/notification.go @@ -19,8 +19,8 @@ import ( // @Summary Get notification // @Description Get notification // @Tags Notification -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param notificationId path string true "Notification ID" @@ -55,8 +55,8 @@ func GetNotification(c *gin.Context) { // @Summary List notifications // @Description List notifications // @Tags Notification -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param all query bool false "List all notifications" @@ -109,8 +109,8 @@ func ListNotifications(c *gin.Context) { // @Summary Update notification // @Description Update notification // @Tags Notification -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param notificationId path string true "Notification ID" @@ -157,8 +157,8 @@ func UpdateNotification(c *gin.Context) { // @Summary Delete notification // @Description Delete notification // @Tags Notification -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param notificationId path string true "Notification ID" diff --git a/routers/api/v2/register.go b/routers/api/v2/register.go index 9cb1828b..b691e7ce 100644 --- a/routers/api/v2/register.go +++ b/routers/api/v2/register.go @@ -16,7 +16,7 @@ import ( // @Summary Register resource // @Description Register resource // @Tags Register -// @Produce json +// @Produce json // @Success 204 // @Failure 400 {object} sys.ErrorResponse // @Failure 401 {object} sys.ErrorResponse diff --git a/routers/api/v2/resource_migration.go b/routers/api/v2/resource_migration.go index 25aaa698..a8dc4170 100644 --- a/routers/api/v2/resource_migration.go +++ b/routers/api/v2/resource_migration.go @@ -19,7 +19,7 @@ import ( // @Summary Get resource migration // @Description Get resource migration // @Tags ResourceMigration -// @Produce json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param resourceMigrationId path string true "Resource Migration ID" @@ -61,7 +61,7 @@ func GetResourceMigration(c *gin.Context) { // @Summary List resource migrations // @Description List resource migrations // @Tags ResourceMigration -// @Produce json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param page query int false "Page number" @@ -109,8 +109,8 @@ func ListResourceMigrations(c *gin.Context) { // @Summary Create resource migration // @Description Create resource migration // @Tags ResourceMigration -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param body body body.ResourceMigrationCreate true "Resource Migration Create" @@ -176,8 +176,8 @@ func CreateResourceMigration(c *gin.Context) { // @Summary Update resource migration // @Description Update resource migration // @Tags ResourceMigration -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param resourceMigrationId path string true "Resource Migration ID" @@ -277,8 +277,8 @@ func UpdateResourceMigration(c *gin.Context) { // @Summary Delete resource migration // @Description Delete resource migration // @Tags ResourceMigration -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param resourceMigrationId path string true "Resource Migration ID" diff --git a/routers/api/v2/snapshot.go b/routers/api/v2/snapshot.go index b8f868b6..78c8658d 100644 --- a/routers/api/v2/snapshot.go +++ b/routers/api/v2/snapshot.go @@ -80,7 +80,7 @@ func GetSnapshot(c *gin.Context) { // @Failure 404 {object} sys.ErrorResponse // @Failure 423 {object} sys.ErrorResponse // @Failure 500 {object} sys.ErrorResponse -// @Router /v2/snapshots [get] +// @Router /v2/snapshots/{vmId} [get] func ListSnapshots(c *gin.Context) { context := sys.NewContext(c) @@ -133,7 +133,7 @@ func ListSnapshots(c *gin.Context) { // @Failure 400 {object} sys.ErrorResponse // @Failure 404 {object} sys.ErrorResponse // @Failure 500 {object} sys.ErrorResponse -// @Router /v2/snapshots [post] +// @Router /v2/snapshots/{vmId} [post] func CreateSnapshot(c *gin.Context) { context := sys.NewContext(c) diff --git a/routers/api/v2/system.go b/routers/api/v2/system.go index 2ca9674c..f827f7eb 100644 --- a/routers/api/v2/system.go +++ b/routers/api/v2/system.go @@ -13,7 +13,7 @@ import ( // @Summary List system capacities // @Description List system capacities // @Tags System -// @Produce json +// @Produce json // @Param n query int false "n" // @Success 200 {array} body.TimestampedSystemCapacities // @Failure 400 {object} body.BindingError @@ -46,7 +46,7 @@ func ListSystemCapacities(c *gin.Context) { // @Summary List system stats // @Description List system stats // @Tags System -// @Produce json +// @Produce json // @Param n query int false "n" // @Success 200 {array} body.TimestampedSystemStats // @Failure 400 {object} body.BindingError @@ -79,7 +79,7 @@ func ListSystemStats(c *gin.Context) { // @Summary List system stats // @Description List system stats // @Tags System -// @Produce json +// @Produce json // @Param n query int false "n" // @Success 200 {array} body.TimestampedSystemStatus // @Failure 400 {object} body.BindingError diff --git a/routers/api/v2/team.go b/routers/api/v2/team.go index f312dde5..44965781 100644 --- a/routers/api/v2/team.go +++ b/routers/api/v2/team.go @@ -27,7 +27,6 @@ import ( // @Summary Get team // @Description Get team // @Tags Team -// @Accept json // @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth @@ -64,7 +63,6 @@ func GetTeam(c *gin.Context) { // @Summary List teams // @Description List teams // @Tags Team -// @Accept json // @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth @@ -366,5 +364,4 @@ func getResourceName(resource *model.TeamResource) *string { } return nil - } diff --git a/routers/api/v2/user.go b/routers/api/v2/user.go index e863879a..818b0e0b 100644 --- a/routers/api/v2/user.go +++ b/routers/api/v2/user.go @@ -20,7 +20,7 @@ import ( // @Summary Get user // @Description Get user // @Tags User -// @Produce json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param userId path string true "User ID" @@ -99,7 +99,7 @@ func GetUser(c *gin.Context) { // @Summary List users // @Description List users // @Tags User -// @Produce json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param all query bool false "List all" @@ -171,8 +171,8 @@ func ListUsers(c *gin.Context) { // @Summary Update user // @Description Update user // @Tags User -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param userId path string true "User ID" diff --git a/routers/api/v2/virtual_routes.go b/routers/api/v2/virtual_routes.go index e3e2b757..d6ee7c30 100644 --- a/routers/api/v2/virtual_routes.go +++ b/routers/api/v2/virtual_routes.go @@ -10,7 +10,7 @@ import "github.com/gin-gonic/gin" // @Summary Get metrics // @Description Get metrics // @Tags Metrics -// @Produce json +// @Produce json // @Success 200 {object} string // @Failure 500 {object} sys.ErrorResponse // @Router /v2/metrics [get] diff --git a/routers/api/v2/vm.go b/routers/api/v2/vm.go index b7087195..e7993a8a 100644 --- a/routers/api/v2/vm.go +++ b/routers/api/v2/vm.go @@ -26,7 +26,7 @@ import ( // @Summary Get VM // @Description Get VM // @Tags VM -// @Produce json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param vmId path string true "VM ID" @@ -180,6 +180,16 @@ func CreateVM(c *gin.Context) { return } + // Just to be sure (redundant) + if auth != nil { + // If the users role has explicitly specified false for useVms then we dont allow it. + // To keep backward compatibility we allow roles without useVms specified to create vms. + if role := auth.GetEffectiveRole(); role.Permissions.UseVms != nil && !*role.Permissions.UseVms { + context.Forbidden("Not permitted to create a VM, missing useVms permission") + return + } + } + deployV2 := service.V2(auth) unique, err := deployV2.VMs().NameAvailable(requestBody.Name) @@ -416,7 +426,6 @@ func UpdateVM(c *gin.Context) { "params": requestBody, "authInfo": auth, }) - if err != nil { context.ServerError(err, ErrInternal) return diff --git a/routers/api/v2/vm_action.go b/routers/api/v2/vm_action.go index 05cbf314..04034b0d 100644 --- a/routers/api/v2/vm_action.go +++ b/routers/api/v2/vm_action.go @@ -16,8 +16,8 @@ import ( // @Summary Creates a new action // @Description Creates a new action // @Tags VmAction -// @Accept json -// @Produce json +// @Accept json +// @Produce json // @Security ApiKeyAuth // @Security KeycloakOAuth // @Param vmId path string true "VM ID" @@ -26,7 +26,7 @@ import ( // @Failure 400 {object} sys.ErrorResponse // @Failure 404 {object} sys.ErrorResponse // @Failure 500 {object} sys.ErrorResponse -// @Router /v2/vmActions [post] +// @Router /v2/vmActions/{vmId} [post] func CreateVmAction(c *gin.Context) { context := sys.NewContext(c) @@ -72,7 +72,6 @@ func CreateVmAction(c *gin.Context) { "params": requestBody, "authInfo": auth, }) - if err != nil { context.ServerError(err, ErrInternal) return diff --git a/routers/api/validators/validators.go b/routers/api/validators/validators.go index 2cafc3c9..11bac180 100644 --- a/routers/api/validators/validators.go +++ b/routers/api/validators/validators.go @@ -392,3 +392,29 @@ func goodURL(url string) bool { } return true } + +var rfc1123SubdomainRE = regexp.MustCompile( + `^[a-z0-9]([\-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9]([\-a-z0-9]*[a-z0-9])?)*$`, +) + +// Rfc1123 validates that the field is a valid RFC1123 subdomain. +// It returns false for empty strings. +func Rfc1123(fl validator.FieldLevel) bool { + // only string fields supported + if fl.Field().Kind().String() != "string" { + return false + } + val := fl.Field().String() + + // reject surrounding whitespace explicitly + if strings.TrimSpace(val) != val { + return false + } + + // empty => invalid here + if val == "" { + return false + } + + return rfc1123SubdomainRE.MatchString(val) +} diff --git a/routers/router.go b/routers/router.go index 7d4b2373..a44d7e7b 100644 --- a/routers/router.go +++ b/routers/router.go @@ -137,6 +137,7 @@ func registerCustomValidators() { registrations := map[string]func(fl validator.FieldLevel) bool{ "rfc1035": validators.Rfc1035, + "rfc1123": validators.Rfc1123, "ssh_public_key": validators.SshPublicKey, "env_name": validators.EnvName, "env_list": validators.EnvList, diff --git a/routers/routes/gpu_claims.go b/routers/routes/gpu_claims.go new file mode 100644 index 00000000..43a84a1e --- /dev/null +++ b/routers/routes/gpu_claims.go @@ -0,0 +1,23 @@ +package routes + +import v2 "github.com/kthcloud/go-deploy/routers/api/v2" + +const ( + GpuClaimsPath = "/v2/gpuClaims" + GpuClaimPath = "/v2/gpuClaims/:gpuClaimId" +) + +type GpuClaimRoutingGroup struct{ RoutingGroupBase } + +func GpuClaimRoutes() *GpuClaimRoutingGroup { + return &GpuClaimRoutingGroup{} +} + +func (group *GpuClaimRoutingGroup) PrivateRoutes() []Route { + return []Route{ + {Method: "GET", Pattern: GpuClaimPath, HandlerFunc: v2.GetGpuClaim}, + {Method: "GET", Pattern: GpuClaimsPath, HandlerFunc: v2.ListGpuClaims}, + {Method: "POST", Pattern: GpuClaimsPath, HandlerFunc: v2.CreateGpuClaim}, + {Method: "DELETE", Pattern: GpuClaimPath, HandlerFunc: v2.DeleteGpuClaim}, + } +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 7638a117..10f6562f 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -24,6 +24,7 @@ func RoutingGroups() []RoutingGroup { return []RoutingGroup{ DiscoverRoutes(), DeploymentRoutes(), + GpuClaimRoutes(), GpuGroupRoutes(), GpuLeaseRoutes(), HostRoutes(), diff --git a/service/clients/v2.go b/service/clients/v2.go index a5e3f3ae..e5237c33 100644 --- a/service/clients/v2.go +++ b/service/clients/v2.go @@ -20,4 +20,5 @@ type V2 interface { ResourceMigrations() apiV2.ResourceMigrations System() apiV2.System VMs() apiV2.VMs + GpuClaims() apiV2.GpuClaims } diff --git a/service/core/cache.go b/service/core/cache.go index 8c8992c6..1853229b 100644 --- a/service/core/cache.go +++ b/service/core/cache.go @@ -8,6 +8,7 @@ type Cache struct { deploymentStore map[string]*model.Deployment vmStore map[string]*model.VM gpuLeaseStore map[string]*model.GpuLease + gpuClaimStore map[string]*model.GpuClaim smStore map[string]*model.SM userStore map[string]*model.User teamStore map[string]*model.Team @@ -23,6 +24,7 @@ func NewCache() *Cache { deploymentStore: make(map[string]*model.Deployment), vmStore: make(map[string]*model.VM), gpuLeaseStore: make(map[string]*model.GpuLease), + gpuClaimStore: make(map[string]*model.GpuClaim), smStore: make(map[string]*model.SM), userStore: make(map[string]*model.User), teamStore: make(map[string]*model.Team), @@ -131,6 +133,29 @@ func (c *Cache) GetGpuLease(id string) *model.GpuLease { return r } +// StoreGpuLease stores a GpuClaim in the cache. +// It only stores the GpuClaim if it is not nil. +func (c *Cache) StoreGpuClaim(gpuClaim *model.GpuClaim) { + if gpuClaim != nil { + if g, ok := c.gpuClaimStore[gpuClaim.ID]; ok { + *g = *gpuClaim + } else { + c.gpuClaimStore[gpuClaim.ID] = gpuClaim + } + } +} + +// GetGpuClaim gets a GpuClaim from the cache. +// It returns nil if the GpuClaim is not in the cache. +func (c *Cache) GetGpuClaim(id string) *model.GpuClaim { + r, ok := c.gpuClaimStore[id] + if !ok { + return nil + } + + return r +} + // StoreSM stores a SM in the cache. // It only stores the SM if it is not nil. // diff --git a/service/errors/errors.go b/service/errors/errors.go index 428c6665..a65d7da6 100644 --- a/service/errors/errors.go +++ b/service/errors/errors.go @@ -1,6 +1,9 @@ package errors -import "fmt" +import ( + "errors" + "fmt" +) // QuotaExceededError is returned when the quota is exceeded. // For instance, if a user creates too many deployments. @@ -182,4 +185,11 @@ var ( // ErrBadMigrationCode is returned when the migration code is invalid. ErrBadMigrationCode = fmt.Errorf("bad migration code") + + // ErrGpuClaimAlreadyExists is returned when the gpu claim already exists. + ErrGpuClaimAlreadyExists = errors.New("gpu claim already exists") + + ErrBadGpuClaim = errors.New("bad gpu claim") + + ErrBadGpuClaimNoRequest = errors.Join(ErrBadGpuClaim, errors.New("no request in claim")) ) diff --git a/service/generators/k8s_generator.go b/service/generators/k8s_generator.go index ef8ff4da..ce705668 100644 --- a/service/generators/k8s_generator.go +++ b/service/generators/k8s_generator.go @@ -28,6 +28,9 @@ type K8sGenerator interface { // PVCs returns a list of models.PvcPublic that should be created PVCs() []models.PvcPublic + // ResourceClaims returns a list of models.ResourceClaimPublic that should be created + ResourceClaims() []models.ResourceClaimPublic + // Secrets returns a list of models.SecretPublic that should be created Secrets() []models.SecretPublic @@ -73,6 +76,10 @@ func (kg *K8sGeneratorBase) PVCs() []models.PvcPublic { return nil } +func (kg *K8sGeneratorBase) ResourceClaims() []models.ResourceClaimPublic { + return nil +} + func (kg *K8sGeneratorBase) Secrets() []models.SecretPublic { return nil } diff --git a/service/utils/utils.go b/service/utils/utils.go index 506b238f..7f9b01e0 100644 --- a/service/utils/utils.go +++ b/service/utils/utils.go @@ -1,12 +1,13 @@ package utils import ( + "reflect" + "time" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/kthcloud/go-deploy/pkg/log" "github.com/kthcloud/go-deploy/pkg/subsystems" - "reflect" - "time" ) // UpdateDbSubsystem is a common interface that services use to update subsystem resources @@ -150,3 +151,13 @@ func GetFirstOrDefault[T any](variable []T) T { defaultVal := new(T) return *defaultVal } + +func FirstNonZero[T comparable](vars ...T) T { + var zero T + for _, v := range vars { + if v != zero { + return v + } + } + return zero +} diff --git a/service/v2/api/api_client.go b/service/v2/api/api_client.go index cae6f12a..926b7117 100644 --- a/service/v2/api/api_client.go +++ b/service/v2/api/api_client.go @@ -10,6 +10,8 @@ import ( "github.com/kthcloud/go-deploy/service/v2/deployments/harbor_service" deploymentK8sService "github.com/kthcloud/go-deploy/service/v2/deployments/k8s_service" dOpts "github.com/kthcloud/go-deploy/service/v2/deployments/opts" + gpuClaimsK8sService "github.com/kthcloud/go-deploy/service/v2/gpu_claims/k8s_service" + gpuClaimOpts "github.com/kthcloud/go-deploy/service/v2/gpu_claims/opts" jobOpts "github.com/kthcloud/go-deploy/service/v2/jobs/opts" nOpts "github.com/kthcloud/go-deploy/service/v2/notifications/opts" resourceMigrationOpts "github.com/kthcloud/go-deploy/service/v2/resource_migrations/opts" @@ -164,8 +166,7 @@ type Snapshots interface { Apply(vmID, id string) error } -type GPUs interface { -} +type GPUs interface{} type GpuLeases interface { Get(id string, opts ...vmOpts.GetGpuLeaseOpts) (*model.GpuLease, error) @@ -202,3 +203,13 @@ type System interface { ListZones(opts ...systemOpts.ListOpts) ([]configModels.Zone, error) ZoneHasCapability(zoneName, capability string) bool } + +type GpuClaims interface { + Get(id string, opts ...gpuClaimOpts.Opts) (*model.GpuClaim, error) + List(opts ...gpuClaimOpts.ListOpts) ([]model.GpuClaim, error) + Create(id string, gpuClaimsCreateParams *model.GpuClaimCreateParams) error + Delete(id string) error + Update(id string, gpuClaimsUpdateParams *model.GpuClaimUpdateParams) error + + K8s() *gpuClaimsK8sService.Client +} diff --git a/service/v2/client.go b/service/v2/client.go index 03053fa8..c7785bda 100644 --- a/service/v2/client.go +++ b/service/v2/client.go @@ -6,6 +6,7 @@ import ( "github.com/kthcloud/go-deploy/service/v2/deployments" "github.com/kthcloud/go-deploy/service/v2/discovery" "github.com/kthcloud/go-deploy/service/v2/events" + "github.com/kthcloud/go-deploy/service/v2/gpu_claims" "github.com/kthcloud/go-deploy/service/v2/jobs" "github.com/kthcloud/go-deploy/service/v2/notifications" "github.com/kthcloud/go-deploy/service/v2/resource_migrations" @@ -84,3 +85,7 @@ func (c *Client) Users() api.Users { func (c *Client) VMs() api.VMs { return vms.New(c, c.cache) } + +func (c *Client) GpuClaims() api.GpuClaims { + return gpu_claims.New(c, c.cache) +} diff --git a/service/v2/deployments/deployment_service.go b/service/v2/deployments/deployment_service.go index 3f5c377a..915b5899 100644 --- a/service/v2/deployments/deployment_service.go +++ b/service/v2/deployments/deployment_service.go @@ -130,6 +130,14 @@ func (c *Client) List(opts ...opts.ListOpts) ([]model.Deployment, error) { drc.WithPagination(o.Pagination.Page, o.Pagination.PageSize) } + if o.GpuClaimName != nil { + if o.GpuClaimRequest != nil { + drc.WithGpuClaimRequest(*o.GpuClaimName, *o.GpuClaimRequest) + } else { + drc.WithGpuClaim(*o.GpuClaimName) + } + } + var effectiveUserID string if o.UserID != nil { // Specific user's deployments are requested @@ -251,6 +259,19 @@ func (c *Client) Create(id, ownerID string, deploymentCreate *body.DeploymentCre return sErrors.NewZoneCapabilityMissingError(params.Zone, configModels.ZoneCapabilityDeployment) } + if len(params.GPUs) > 0 { + if !c.V2.System().ZoneHasCapability(params.Zone, configModels.ZoneCapabilityDRA) { + return sErrors.NewZoneCapabilityMissingError(params.Zone, configModels.ZoneCapabilityDRA) + } + + // TODO: get the roles of the user to verify that the claims exist + /*var roles = make([]string, 0, 2) + + c.V2.GpuClaims().List(gpuClaimOpts.List{ + Roles: + })*/ + } + deployment, err := deployment_repo.New().Create(id, ownerID, params) if err != nil { if errors.Is(err, rErrors.ErrNonUniqueField) { @@ -624,6 +645,7 @@ func (c *Client) CheckQuota(id string, opts *opts.QuotaOptions) error { var replicas int var cpu float64 var ram float64 + var gpus int if opts.Create.Replicas != nil { replicas = *opts.Create.Replicas @@ -643,6 +665,10 @@ func (c *Client) CheckQuota(id string, opts *opts.QuotaOptions) error { ram = usage.RAM + config.Config.Deployment.Resources.Limits.RAM*float64(replicas) } + if opts.Create.GPUs != nil { + gpus = usage.Gpus + len(opts.Create.GPUs) + } + if cpu > quota.CpuCores { return sErrors.NewQuotaExceededError(fmt.Sprintf("CPU quota exceeded. Current: %.1f, Quota: %.1f", cpu, quota.CpuCores)) } @@ -651,6 +677,10 @@ func (c *Client) CheckQuota(id string, opts *opts.QuotaOptions) error { return sErrors.NewQuotaExceededError(fmt.Sprintf("RAM quota exceeded. Current: %.1f, Quota: %.1f", ram, quota.RAM)) } + if gpus > quota.Gpus { + return sErrors.NewQuotaExceededError(fmt.Sprintf("GPU quota exceeded. Current: %d, Quota: %d", gpus, quota.Gpus)) + } + return nil } else if opts.Update != nil { deployment, err := c.Get(id) @@ -665,10 +695,12 @@ func (c *Client) CheckQuota(id string, opts *opts.QuotaOptions) error { replicasBefore := deployment.GetMainApp().Replicas cpuBefore := deployment.GetMainApp().CpuCores * float64(replicasBefore) ramBefore := deployment.GetMainApp().RAM * float64(replicasBefore) + gpusBefore := len(deployment.GetMainApp().GPUs) * replicasBefore var replicasAfter int var cpuAfter float64 var ramAfter float64 + var gpusAfter int if opts.Update.Replicas != nil { replicasAfter = *opts.Update.Replicas @@ -688,6 +720,12 @@ func (c *Client) CheckQuota(id string, opts *opts.QuotaOptions) error { ramAfter = usage.RAM + deployment.GetMainApp().RAM*float64(replicasAfter) - ramBefore } + if opts.Update.GPUs != nil { + gpusAfter = usage.Gpus + len(*opts.Update.GPUs)*replicasAfter - gpusBefore + } else { + gpusAfter = usage.Gpus + len(deployment.GetMainApp().GPUs)*replicasAfter - gpusBefore + } + if cpuAfter > quota.CpuCores { return sErrors.NewQuotaExceededError(fmt.Sprintf("CPU quota exceeded. Current: %.1f, Quota: %.1f", cpuAfter, quota.CpuCores)) } @@ -695,6 +733,10 @@ func (c *Client) CheckQuota(id string, opts *opts.QuotaOptions) error { return sErrors.NewQuotaExceededError(fmt.Sprintf("RAM quota exceeded. Current: %.1f, Quota: %.1f", ramAfter, quota.RAM)) } + if gpusAfter > quota.Gpus { + return sErrors.NewQuotaExceededError(fmt.Sprintf("GPU quota exceeded. Current: %d, Quota: %d", gpusAfter, quota.Gpus)) + } + return nil } else { log.Println("Quota options not set when checking quota for deployment", id) diff --git a/service/v2/deployments/opts/opts.go b/service/v2/deployments/opts/opts.go index 7bafcdc3..a1644af2 100644 --- a/service/v2/deployments/opts/opts.go +++ b/service/v2/deployments/opts/opts.go @@ -29,6 +29,8 @@ type ListOpts struct { GitHubWebhookID *int64 UserID *string Shared bool + GpuClaimName *string + GpuClaimRequest *string } // GetOpts is used to specify the options when getting a deployment. diff --git a/service/v2/deployments/resources/k8s_generator.go b/service/v2/deployments/resources/k8s_generator.go index 3a4dd4f2..7d4fe3b4 100644 --- a/service/v2/deployments/resources/k8s_generator.go +++ b/service/v2/deployments/resources/k8s_generator.go @@ -106,6 +106,33 @@ func (kg *K8sGenerator) Deployments() []models.DeploymentPublic { } } + k8sResClaims := make([]models.DynamicResourceClaim, 0, len(mainApp.GPUs)) + for _, gpu := range mainApp.GPUs { + rc := models.DynamicResourceClaim{ + Name: fmt.Sprintf("%s-%s", kg.deployment.Name, makeValidK8sName(gpu.Name)), + Request: []string{gpu.Name}, + } + + if gpu.ClaimName != "" { + rc.ResourceClaimName = &gpu.ClaimName + } else { + // needs to have one of them + continue + } + + k8sResClaims = append(k8sResClaims, rc) + } + + // TODO: make this more dynamic, dont just support nvidia + tolerations := make([]models.Toleration, 0, max(1, len(mainApp.GPUs))) + if len(mainApp.GPUs) > 0 { + tolerations = append(tolerations, models.Toleration{ + Key: "nvidia.com/gpu", + Operator: "Exists", + Effect: "NoSchedule", + }) + } + res := make([]models.DeploymentPublic, 0) dep := models.DeploymentPublic{ @@ -130,6 +157,8 @@ func (kg *K8sGenerator) Deployments() []models.DeploymentPublic { InitCommands: mainApp.InitCommands, InitContainers: make([]models.InitContainer, 0), Volumes: k8sVolumes, + ResourceClaims: k8sResClaims, + Tolerations: tolerations, Disabled: mainApp.Replicas == 0, } @@ -737,7 +766,7 @@ func encodeDockerConfig(registry, username, password string) []byte { // getExternalFQDN returns the external FQDN for a deployment in a given zone func getExternalFQDN(name string, zone *configModels.Zone) string { // Remove protocol:// and :port from the zone.Domains.ParentDeployment - var fqdn = zone.Domains.ParentDeployment + fqdn := zone.Domains.ParentDeployment split := strings.Split(zone.Domains.ParentDeployment, "://") if len(split) > 1 { diff --git a/service/v2/gpu_claims/api_client.go b/service/v2/gpu_claims/api_client.go new file mode 100644 index 00000000..55cac9ce --- /dev/null +++ b/service/v2/gpu_claims/api_client.go @@ -0,0 +1,35 @@ +package gpu_claims + +import ( + "github.com/kthcloud/go-deploy/service/clients" + "github.com/kthcloud/go-deploy/service/core" + "github.com/kthcloud/go-deploy/service/v2/gpu_claims/client" + "github.com/kthcloud/go-deploy/service/v2/gpu_claims/k8s_service" +) + +// Client is the client for the Deployment service. +// It is used as a wrapper around the Client. +type Client struct { + V2 clients.V2 + + client.BaseClient[Client] +} + +// New creates a new VM service client. +func New(v2 clients.V2, cache ...*core.Cache) *Client { + var ca *core.Cache + if len(cache) > 0 { + ca = cache[0] + } else { + ca = core.NewCache() + } + + c := &Client{V2: v2, BaseClient: client.NewBaseClient[Client](ca)} + c.BaseClient.SetParent(c) + return c +} + +// K8s returns the client for the K8s service. +func (c *Client) K8s() *k8s_service.Client { + return k8s_service.New(c.Cache) +} diff --git a/service/v2/gpu_claims/client/client.go b/service/v2/gpu_claims/client/client.go new file mode 100644 index 00000000..321903b3 --- /dev/null +++ b/service/v2/gpu_claims/client/client.go @@ -0,0 +1,107 @@ +package client + +import ( + "fmt" + + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/pkg/db/resources/gpu_claim_repo" + "github.com/kthcloud/go-deploy/service/core" +) + +// BaseClient is the base client for all the subsystems client for GPUClaims. +type BaseClient[parent any] struct { + p *parent + + // Cache is used to cache the resources fetched inside the service. + Cache *core.Cache +} + +// NewBaseClient creates a new BaseClient. +func NewBaseClient[parent any](cache *core.Cache) BaseClient[parent] { + if cache == nil { + cache = core.NewCache() + } + + return BaseClient[parent]{Cache: cache} +} + +// SetParent sets the parent of the client. +// This ensures the correct parent client is returned when calling builder methods. +func (c *BaseClient[parent]) SetParent(p *parent) { + c.p = p +} + +// GpuClaim returns the GpuClaim with the given ID. +// After a successful fetch, the GpuClaim will be cached. +func (c *BaseClient[parent]) GpuClaim(id string, gcc *gpu_claim_repo.Client) (*model.GpuClaim, error) { + gc := c.Cache.GetGpuClaim(id) + if gc != nil { + return gc, nil + } + + return c.fetchGpuClaim(id, gcc) +} + +// GpuClaim returns the GpuClaim with the given ID. +// After a successful fetch, the GpuClaim will be cached. +func (c *BaseClient[parent]) GpuClaims(gcc *gpu_claim_repo.Client) ([]model.GpuClaim, error) { + return c.fetchGpuClaims(gcc) +} + +// Refresh clears the cache for the GpuClaim with the given ID and fetches it again. +// After a successful fetch, the GpuClaim is cached. +func (c *BaseClient[parent]) Refresh(id string) (*model.GpuClaim, error) { + return c.fetchGpuClaim(id, nil) +} + +func (c *BaseClient[parent]) fetchGpuClaims(gcc *gpu_claim_repo.Client) ([]model.GpuClaim, error) { + makeError := func(err error) error { + return fmt.Errorf("failed to fetch gpu claims in service client: %w", err) + } + + if gcc == nil { + gcc = gpu_claim_repo.New() + } + + gcs, err := gcc.List() + if err != nil { + return nil, makeError(err) + } + + for _, gc := range gcs { + v := gc + c.Cache.StoreGpuClaim(&v) + } + + return gcs, nil +} + +func (c *BaseClient[parent]) fetchGpuClaim(id string, gcc *gpu_claim_repo.Client) (*model.GpuClaim, error) { + makeError := func(err error) error { + return fmt.Errorf("failed to fetch gpu claim in service client: %w", err) + } + + if gcc == nil { + gcc = gpu_claim_repo.New() + } + + var gc *model.GpuClaim + var err error + + if id == "" { + gc, err = gcc.Get() + } else { + gc, err = gcc.GetByID(id) + } + + if err != nil { + return nil, makeError(err) + } + + if gc == nil { + return nil, nil + } + + c.Cache.StoreGpuClaim(gc) + return gc, nil +} diff --git a/service/v2/gpu_claims/gc_service.go b/service/v2/gpu_claims/gc_service.go new file mode 100644 index 00000000..1a6d5121 --- /dev/null +++ b/service/v2/gpu_claims/gc_service.go @@ -0,0 +1,359 @@ +package gpu_claims + +import ( + "errors" + "fmt" + "slices" + "strings" + + "github.com/google/uuid" + "github.com/kthcloud/go-deploy/dto/v2/body" + modelConfig "github.com/kthcloud/go-deploy/models/config" + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/models/version" + "github.com/kthcloud/go-deploy/pkg/config" + "github.com/kthcloud/go-deploy/pkg/db/resources/gpu_claim_repo" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/parsers/dra/nvidia" + sErrors "github.com/kthcloud/go-deploy/service/errors" + serviceUtils "github.com/kthcloud/go-deploy/service/utils" + deploymentOpts "github.com/kthcloud/go-deploy/service/v2/deployments/opts" + "github.com/kthcloud/go-deploy/service/v2/gpu_claims/k8s_service" + "github.com/kthcloud/go-deploy/service/v2/gpu_claims/opts" + + "github.com/kthcloud/go-deploy/pkg/log" +) + +// Get detailed gpu claims +// +// Only admin users are allowed to get the detailed gpu claim +func (c *Client) Get(id string, opts ...opts.Opts) (*model.GpuClaim, error) { + o := serviceUtils.GetFirstOrDefault(opts) + + gcrc := gpu_claim_repo.New() + + if c.V2.Auth() == nil || !c.V2.Auth().User.IsAdmin { + return nil, sErrors.ErrForbidden + } + + if o.Zone != nil { + gcrc.WithZone(o.Zone.Name) + } + + return c.GpuClaim(id, gcrc) +} + +func (c *Client) List(opts ...opts.ListOpts) ([]model.GpuClaim, error) { + o := serviceUtils.GetFirstOrDefault(opts) + + gcrc := gpu_claim_repo.New() + + if o.Pagination != nil { + gcrc.WithPagination(o.Pagination.Page, o.Pagination.PageSize) + } + + if o.Zone != nil { + gcrc.WithZone(*o.Zone) + } + + if o.Roles != nil && !slices.Contains(*o.Roles, "admin") { + gcrc.WithRoles(*o.Roles) + } + + if o.Names != nil { + gcrc.WithNames(*o.Names) + } + + return c.GpuClaims(gcrc) +} + +// Create creates a new gpu claim +func (c *Client) Create(id string, params *model.GpuClaimCreateParams) error { + makeErr := func(err error) error { + return fmt.Errorf("failed to create gpu claim. details: %w", err) + } + + if params.Zone = strings.TrimSpace(params.Zone); params.Zone == "" { + params.Zone = config.Config.Deployment.DefaultZone + } + + if !c.V2.System().ZoneHasCapability(params.Zone, modelConfig.ZoneCapabilityDRA) { + return sErrors.NewZoneCapabilityMissingError(params.Zone, modelConfig.ZoneCapabilityDRA) + } + + if len(params.Requested) < 1 { + return sErrors.ErrBadGpuClaimNoRequest + } else { + for _, req := range params.Requested { + if req.Config != nil { + if strings.TrimSpace(req.Config.Driver) == "" { + return makeErr(fmt.Errorf("config provided but driver is empty")) + } else { + log.Println("driver: ", req.Config.Driver) + } + } + } + } + + err := gpu_claim_repo.New().Create(id, params) + if err != nil { + if errors.Is(err, gpu_claim_repo.ErrGpuClaimAlreadyExists) { + return sErrors.ErrGpuClaimAlreadyExists + } + + return makeErr(err) + } + + err = k8s_service.New(c.Cache).Create(id, params) + if err != nil { + return makeErr(err) + } + + return nil +} + +// Delete deletes an existing gpu claim +// It will best effort delete references to the claim from deployments. +func (c *Client) Delete(id string) error { + makeErr := func(err error) error { + return fmt.Errorf("failed to delete gpu claim. details: %w", err) + } + + repo := gpu_claim_repo.New() + + claim, err := repo.GetByID(id) + if err != nil || claim == nil { + if err != nil { + return makeErr(err) + } + if claim == nil { + return makeErr(sErrors.ErrResourceNotFound) + } + } + + // Best effort remove references to the resourceClaims when deleting the claim + // We do this because otherwise the pods of the deployment will be struck on + // creating when the claims are removed from k8s. + depls, err := c.V2.Deployments().List(deploymentOpts.ListOpts{ + GpuClaimName: &claim.Name, + }) + if err != nil { + log.Warnf("Could not cascade references to resourceclaim, err: %s", err.Error()) + } + if len(depls) > 0 { + for _, depl := range depls { + mainApp := depl.GetMainApp() + gpusToKeep := make([]body.DeploymentGPU, 0, len(mainApp.GPUs)) + + for _, gpu := range mainApp.GPUs { + if gpu.ClaimName != claim.Name { + gpusToKeep = append(gpusToKeep, body.DeploymentGPU{ + Name: gpu.Name, + ClaimName: gpu.ClaimName, + }) + } + } + + update := body.DeploymentUpdate{ + GPUs: &gpusToKeep, + } + + jobUUID, err := uuid.NewRandom() + if err != nil { + log.Errorf("Could not generate uuid v4 for update job, err: %s", err.Error()) + continue + } + if err := c.V2.Jobs().Create(jobUUID.String(), depl.OwnerID, model.JobUpdateDeployment, version.V2, map[string]any{ + "id": depl.ID, + "params": update, + }); err != nil { + log.Warnf("Could not create update job for deployment: %s, reason: %s", depl.ID, err) + continue + } + + } + } + + err = k8s_service.New(c.Cache).Delete(id) + if err != nil { + return makeErr(err) + } + + if err := repo.DeleteByID(id); err != nil { + return makeErr(err) + } + + return nil +} + +func (c *Client) Update(id string, params *model.GpuClaimUpdateParams) error { + makeErr := func(err error) error { + return fmt.Errorf("failed to update gpu claim. details: %w", err) + } + repo := gpu_claim_repo.New() + + claim, err := repo.GetByID(id) + if err != nil || claim == nil { + if err != nil { + return makeErr(err) + } + if claim == nil { + return makeErr(sErrors.ErrResourceNotFound) + } + } + + // Prevent unneccesary k8s updates + needsK8sUpdate := false + if params.Name != nil && *params.Name != claim.Name || params.Zone != nil && *params.Zone != claim.Zone { + needsK8sUpdate = true + } else if params.Requested != nil && requestDiff(*params.Requested, claim.Requested) { + needsK8sUpdate = true + } + + if needsK8sUpdate { + // TODO: impl update for k8s + } + + return nil +} + +// requestDiff checks if there are diffs in the requests for gpus +// Used to determine if k8s update is neccesary or if its fine to omit it and +// just update the db state +func requestDiff(newReqs []model.RequestedGpuCreate, oldReqs map[string]model.RequestedGpu) bool { + if len(newReqs) != len(oldReqs) { + return true + } + for i := range newReqs { + key := newReqs[i].Name + if _, found := oldReqs[key]; !found { + return true + } + + if oldReqs[key].AllocationMode != newReqs[i].AllocationMode { + return true + } + + if newReqs[i].AllocationMode == model.RequestAllocationMode_ExactCount { + if oldReqs[key].Count != nil && newReqs[i].Count != nil { + if *oldReqs[key].Count != *newReqs[i].Count { + return true + } + } else if oldReqs[key].Count != newReqs[i].Count { // one is nil so we just check if both are + return true + } + } + + if oldReqs[key].DeviceClassName != newReqs[i].DeviceClassName { + return true + } + + if len(oldReqs[key].Selectors) != len(newReqs[i].Selectors) { + return true + } else { + for j := range oldReqs[key].Selectors { + if oldReqs[key].Selectors[j] != newReqs[i].Selectors[j] { + return true + } + } + } + + // Check if ther is a diff in the Capacity + if len(oldReqs[key].Capacity) != len(newReqs[i].Capacity) { + return true + } + for k, v := range newReqs[i].Capacity { + if vv, exists := oldReqs[key].Capacity[k]; exists { + if v != vv { + return true + } + } else { + return true + } + } + + // Check if there is a diff in the config + if oldReqs[key].Config != nil && newReqs[i].Config != nil { + oldCfg := *oldReqs[key].Config + newCfg := *newReqs[i].Config + if oldCfg.Driver != newCfg.Driver { + return true + } + if oldCfg.Parameters != nil && newCfg.Parameters != nil { + if oldNvParams, ok := oldCfg.Parameters.(nvidia.GPUConfigParametersImpl); ok { + if newNvParams, ok := newCfg.Parameters.(nvidia.GPUConfigParametersImpl); ok { + + if oldNvParams.APIVersion != newNvParams.APIVersion || oldNvParams.Kind != newNvParams.Kind { + return true + } + if oldNvParams.Sharing != nil && newNvParams.Sharing != nil { + oldSharing := *oldNvParams.Sharing + newSharing := *newNvParams.Sharing + + if newSharing.Strategy != oldSharing.Strategy { + return true + } + + if oldSharing.MpsConfig != nil && newSharing.MpsConfig != nil { + if oldSharing.MpsConfig.DefaultActiveThreadPercentage != nil && newSharing.MpsConfig.DefaultActiveThreadPercentage != nil { + if *oldSharing.MpsConfig.DefaultActiveThreadPercentage != *newSharing.MpsConfig.DefaultActiveThreadPercentage { + return true + } + } else if oldSharing.MpsConfig.DefaultActiveThreadPercentage != newSharing.MpsConfig.DefaultActiveThreadPercentage { // one is nil so we just check if both are + return true + } + if len(oldSharing.MpsConfig.DefaultPerDevicePinnedMemoryLimit) != len(newSharing.MpsConfig.DefaultPerDevicePinnedMemoryLimit) { + return true + } else { + for k, v := range newSharing.MpsConfig.DefaultPerDevicePinnedMemoryLimit { + if vv, exists := oldSharing.MpsConfig.DefaultPerDevicePinnedMemoryLimit[k]; exists { + if v.String() != vv.String() { + return true + } + } else { + return true + } + } + } + if oldSharing.MpsConfig.DefaultPinnedDeviceMemoryLimit != nil && newSharing.MpsConfig.DefaultPinnedDeviceMemoryLimit != nil { + if oldSharing.MpsConfig.DefaultPinnedDeviceMemoryLimit.String() != newSharing.MpsConfig.DefaultPinnedDeviceMemoryLimit.String() { + return true + } + } else if oldSharing.MpsConfig.DefaultPinnedDeviceMemoryLimit != newSharing.MpsConfig.DefaultPinnedDeviceMemoryLimit { // one is nil so we just check if both are + return true + } + } else if oldSharing.MpsConfig != newSharing.MpsConfig { // one is nil so we just check if both are + return true + } + if oldSharing.TimeSlicingConfig != nil && newSharing.TimeSlicingConfig != nil { + if oldSharing.TimeSlicingConfig.Interval != nil && newSharing.TimeSlicingConfig != nil { + if oldSharing.TimeSlicingConfig.Interval != nil && newSharing.TimeSlicingConfig.Interval != nil { + if *oldSharing.TimeSlicingConfig.Interval != *newSharing.TimeSlicingConfig.Interval { + return true + } + } else if oldSharing.TimeSlicingConfig.Interval != newSharing.TimeSlicingConfig.Interval { // one is nil so we just check if both are + return true + } + } else if oldSharing.TimeSlicingConfig.Interval != newSharing.TimeSlicingConfig.Interval { // one is nil so we just check if both are + return true + } + } else if oldSharing.TimeSlicingConfig != newSharing.TimeSlicingConfig { // one is nil so we just check if both are + return true + } + } else if oldNvParams.Sharing != newNvParams.Sharing { // one is nil so we just check if both are + return true + } + } else { + return true // one is nvidia the other is not + } + } else if oldCfg.Parameters.MetaAPIVersion() != newCfg.Parameters.MetaAPIVersion() || oldCfg.Parameters.MetaKind() != newCfg.Parameters.MetaKind() { + return true + } + } else if oldCfg.Parameters != newCfg.Parameters { // one is nil so we just check if both are + return true + } + } else if oldReqs[key].Config != newReqs[i].Config { // one is nil so we just check if both are + return true + } + } + return false +} diff --git a/service/v2/gpu_claims/k8s_service/client.go b/service/v2/gpu_claims/k8s_service/client.go new file mode 100644 index 00000000..738326a3 --- /dev/null +++ b/service/v2/gpu_claims/k8s_service/client.go @@ -0,0 +1,169 @@ +package k8s_service + +import ( + configModels "github.com/kthcloud/go-deploy/models/config" + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/pkg/config" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s" + "github.com/kthcloud/go-deploy/service/core" + sErrors "github.com/kthcloud/go-deploy/service/errors" + "github.com/kthcloud/go-deploy/service/generators" + "github.com/kthcloud/go-deploy/service/v2/gpu_claims/client" + "github.com/kthcloud/go-deploy/service/v2/gpu_claims/opts" + "github.com/kthcloud/go-deploy/service/v2/gpu_claims/resources" +) + +// OptsAll returns the options required to get all the service tools, ie. GPUClaim, client, and generator. +func OptsAll(claimID string, overwriteOps ...opts.ExtraOpts) *opts.Opts { + var eo opts.ExtraOpts + if len(overwriteOps) > 0 { + eo = overwriteOps[0] + } + + return &opts.Opts{ + ClaimID: claimID, + Client: true, + Generator: true, + ExtraOpts: eo, + } +} + +// OptsNoGenerator returns the options required to get the SM and client. +func OptsNoGenerator(rcID string, overwriteOps ...opts.ExtraOpts) *opts.Opts { + var eo opts.ExtraOpts + if len(overwriteOps) > 0 { + eo = overwriteOps[0] + } + return &opts.Opts{ + ClaimID: rcID, + Client: true, + ExtraOpts: eo, + } +} + +// Client is the client for the Harbor service. +// It contains a BaseClient, which is used to lazy-load and cache data. +type Client struct { + client.BaseClient[Client] + + client *k8s.Client + generator *generators.K8sGenerator +} + +// New creates a new Client and injects the cache. +// If a cache is not supplied it will create a new one. +func New(cache ...*core.Cache) *Client { + var ca *core.Cache + if len(cache) > 0 { + ca = cache[0] + } else { + ca = core.NewCache() + } + + c := &Client{BaseClient: client.NewBaseClient[Client](ca)} + c.BaseClient.SetParent(c) + return c +} + +// Get returns the deployment, client, and generator. +// +// Depending on the options specified, some return values may be nil. +// This is useful when you don't always need all the resources. +func (c *Client) Get(opts *opts.Opts) (*model.GpuClaim, *k8s.Client, generators.K8sGenerator, error) { + var gc *model.GpuClaim + var k8sClient *k8s.Client + var k8sGenerator generators.K8sGenerator + var err error + + if opts.ClaimID != "" { + gc, err = c.GpuClaim(opts.ClaimID, nil) + if err != nil { + return nil, nil, nil, err + } + + if gc == nil { + return nil, nil, nil, sErrors.ErrResourceNotFound + } + } + + if opts.Client { + var zone *configModels.Zone + if opts.ExtraOpts.Zone != nil { + zone = opts.ExtraOpts.Zone + } else if gc != nil { + zone = config.Config.GetZone(gc.Zone) + } + + if zone == nil { + return nil, nil, nil, sErrors.ErrZoneNotFound + } + + k8sClient, err = c.Client(zone) + if err != nil { + return nil, nil, nil, err + } + + if k8sClient == nil { + return nil, nil, nil, sErrors.ErrSmNotFound + } + } + + if opts.Generator { + var zone *configModels.Zone + if opts.ExtraOpts.Zone != nil { + zone = opts.ExtraOpts.Zone + } else if gc != nil { + zone = config.Config.GetZone(gc.Zone) + } + + k8sGenerator = c.Generator(gc, k8sClient, zone) + if k8sGenerator == nil { + return nil, nil, nil, sErrors.ErrSmNotFound + } + } + + return gc, k8sClient, k8sGenerator, nil +} + +// Client returns the K8s service client. +func (c *Client) Client(zone *configModels.Zone) (*k8s.Client, error) { + return withClient(zone, getNamespaceName(zone)) +} + +// Generator returns the K8s generator. +func (c *Client) Generator(gc *model.GpuClaim, client *k8s.Client, zone *configModels.Zone) generators.K8sGenerator { + if gc == nil { + panic("cpuclaim is nil") + } + + if client == nil { + panic("client is nil") + } + + if zone == nil { + panic("gpuclaim zone is nil") + } + + return resources.K8s(gc, zone, client, getNamespaceName(zone)) +} + +func OptsOnlyClient(zone *configModels.Zone) *opts.Opts { + return &opts.Opts{ + Client: true, + ExtraOpts: opts.ExtraOpts{Zone: zone}, + } +} + +// getNamespaceName returns the namespace name +func getNamespaceName(zone *configModels.Zone) string { + return zone.K8s.Namespaces.Deployment +} + +// withClient returns a new K8s service client. +func withClient(zone *configModels.Zone, namespace string) (*k8s.Client, error) { + return k8s.New(&k8s.ClientConf{ + K8sClient: zone.K8s.Client, + KubeVirtK8sClient: zone.K8s.KubeVirtClient, + Namespace: namespace, + }) +} diff --git a/service/v2/gpu_claims/k8s_service/k8s_service.go b/service/v2/gpu_claims/k8s_service/k8s_service.go new file mode 100644 index 00000000..4c1e5910 --- /dev/null +++ b/service/v2/gpu_claims/k8s_service/k8s_service.go @@ -0,0 +1,106 @@ +package k8s_service + +import ( + "context" + "fmt" + + "github.com/kthcloud/go-deploy/models/config" + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/pkg/db/resources/gpu_claim_repo" + k8sModels "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/models" + "github.com/kthcloud/go-deploy/service/resources" + "github.com/kthcloud/go-deploy/utils/versionutils" +) + +func (c *Client) Create(id string, params *model.GpuClaimCreateParams) error { + makeError := func(err error) error { + return fmt.Errorf("failed to create gpu claim in k8s. details: %w", err) + } + + _, kc, g, err := c.Get(OptsAll(id)) + if err != nil { + return makeError(err) + } + + v, err := kc.K8sClient.Discovery().ServerVersion() + if err != nil { + return makeError(fmt.Errorf("could not get k8s version: %v", err)) + } + + supportsDRA, err := versionutils.HasStableDRASupport(v) + if err != nil { + return makeError(fmt.Errorf("version parsing error: %v", err)) + } + if !supportsDRA { + return makeError(fmt.Errorf("doesnt support dynamic resource allocation, server is on: %s, dra requires at least: %s", v.String(), versionutils.DRASupportMinStable.String())) + } + + // Namespace + err = resources.SsCreator(kc.CreateNamespace). + WithDbFunc(dbFunc(id, "namespace")). + WithPublic(g.Namespace()). + Exec() + if err != nil { + return makeError(err) + } + + for _, public := range g.ResourceClaims() { + err = resources.SsCreator(kc.CreateResourceClaim). + WithDbFunc(dbFunc(id, "resourceClaimMap."+public.Name)). + WithPublic(&public). + Exec() + if err != nil { + return makeError(err) + } + } + + return nil +} + +func (c *Client) Delete(id string) error { + makeError := func(err error) error { + return fmt.Errorf("failed to delete gpu claim in k8s. details: %w", err) + } + + gc, kc, _, err := c.Get(OptsNoGenerator(id)) + if err != nil { + return makeError(err) + } + + for mapName, rc := range gc.Subsystems.K8s.ResourceClaimMap { + err = resources.SsDeleter(kc.DeleteResourceClaim). + WithResourceID(rc.Name). + WithDbFunc(dbFunc(id, "resourceClaimMap."+mapName)). + Exec() + } + + // Namespace + // (not deleted in k8s, since it is shared) + err = resources.SsDeleter(func(string) error { return nil }). + WithResourceID(gc.Subsystems.K8s.Namespace.Name). + WithDbFunc(dbFunc(id, "namespace")). + Exec() + if err != nil { + return makeError(err) + } + + return nil +} + +// dbFunc returns a function that updates the K8s subsystem. +func dbFunc(id, key string) func(any) error { + return func(data any) error { + if data == nil { + return gpu_claim_repo.New().DeleteSubsystem(id, "k8s."+key) + } + return gpu_claim_repo.New().SetSubsystem(id, "k8s."+key, data) + } +} + +func (c *Client) SetupResourceClaimWatcher(ctx context.Context, zone *config.Zone, yield func(name string, status k8sModels.ResourceClaimStatus, action string)) error { + _, kc, _, err := c.Get(OptsOnlyClient(zone)) + if err != nil { + return err + } + return kc.SetupResourceClaimWatcher(ctx, yield) +} diff --git a/service/v2/gpu_claims/opts/opts.go b/service/v2/gpu_claims/opts/opts.go new file mode 100644 index 00000000..0a0fd3ed --- /dev/null +++ b/service/v2/gpu_claims/opts/opts.go @@ -0,0 +1,33 @@ +package opts + +import ( + configModels "github.com/kthcloud/go-deploy/models/config" + "github.com/kthcloud/go-deploy/service/v2/utils" +) + +// Opts is used to specify which resources to get. +type Opts struct { + ClaimID string + Client bool + Generator bool + + ExtraOpts +} + +// ExtraOpts is used to specify the extra options when getting a GpuClaim. +// This is useful when overwriting the implicit options +type ExtraOpts struct { + Zone *configModels.Zone +} + +// ListOpts is used to specify the options when listing gpu claim. +type ListOpts struct { + Pagination *utils.Pagination + All bool + Roles *[]string + Zone *string + Names *[]string +} + +// GetOpts is used to specify the options when getting a gpu claim. +type GetOpts struct{} diff --git a/service/v2/gpu_claims/resources/k8s_generator.go b/service/v2/gpu_claims/resources/k8s_generator.go new file mode 100644 index 00000000..cd6f38b9 --- /dev/null +++ b/service/v2/gpu_claims/resources/k8s_generator.go @@ -0,0 +1,103 @@ +package resources + +import ( + "log" + + configModels "github.com/kthcloud/go-deploy/models/config" + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/pkg/subsystems" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s" + "github.com/kthcloud/go-deploy/pkg/subsystems/k8s/models" + "github.com/kthcloud/go-deploy/service/generators" + "github.com/kthcloud/go-deploy/utils" + v1 "k8s.io/api/resource/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +type K8sGenerator struct { + generators.K8sGeneratorBase + + image *string + namespace string + client *k8s.Client + + gc *model.GpuClaim + zone *configModels.Zone +} + +func K8s(gc *model.GpuClaim, zone *configModels.Zone, client *k8s.Client, namespace string) *K8sGenerator { + kg := &K8sGenerator{ + namespace: namespace, + client: client, + gc: gc, + zone: zone, + } + + return kg +} + +func (kg *K8sGenerator) Namespace() *models.NamespacePublic { + ns := models.NamespacePublic{ + Name: kg.namespace, + } + + if n := &kg.gc.Subsystems.K8s.Namespace; subsystems.Created(n) { + ns.CreatedAt = n.CreatedAt + } + + return &ns +} + +func (kg *K8sGenerator) ResourceClaims() []models.ResourceClaimPublic { + if kg.gc == nil { + return nil + } + + var rc models.ResourceClaimPublic = models.ResourceClaimPublic{ + Name: kg.gc.Name, + Namespace: kg.namespace, + DeviceRequests: make([]models.ResourceClaimDeviceRequestPublic, 0, len(kg.gc.Requested)), + } + + log.Printf("in k8s_generator: len(kg.gc.Requested) = %d\n", len(kg.gc.Requested)) + + for name, req := range kg.gc.Requested { + rcdr := models.ResourceClaimDeviceRequestPublic{ + Name: name, + // TODO: add request first available in the future if it is wanted + } + + rcer := &models.ResourceClaimExactlyRequestPublic{ + ResourceClaimBaseRequestPublic: models.ResourceClaimBaseRequestPublic{ + AllocationMode: string(req.AllocationMode), + Count: utils.ZeroDeref(req.Count), + DeviceClassName: req.DeviceClassName, + SelectorCelExprs: req.Selectors, + }, + } + rcer.CapacityRequests = make(map[v1.QualifiedName]resource.Quantity, len(req.Capacity)) + for k, v := range req.Capacity { + qty, err := resource.ParseQuantity(v) + if err != nil { + // skip bad format + continue + } + rcer.CapacityRequests[v1.QualifiedName(k)] = qty + } + + rcdr.RequestsExactly = rcer + if req.Config != nil { + rcc := &models.ResourceClaimOpaquePublic{ + Driver: req.Config.Driver, + } + if req.Config.Parameters != nil { + rcc.Parameters = req.Config.Parameters + } + rcdr.Config = rcc + } + + rc.DeviceRequests = append(rc.DeviceRequests, rcdr) + } + + return []models.ResourceClaimPublic{rc} +} diff --git a/service/v2/gpu_claims/resources/k8s_generator_test.go b/service/v2/gpu_claims/resources/k8s_generator_test.go new file mode 100644 index 00000000..f5224601 --- /dev/null +++ b/service/v2/gpu_claims/resources/k8s_generator_test.go @@ -0,0 +1,86 @@ +package resources + +import ( + "reflect" + "testing" + + "github.com/kthcloud/go-deploy/models/model" + "github.com/kthcloud/go-deploy/utils" + v1 "k8s.io/api/resource/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +func TestResourceClaims(t *testing.T) { + count := int64(2) + gc := &model.GpuClaim{ + Name: "gpu-claim", + Requested: map[string]model.RequestedGpu{ + "gpu1": { + + AllocationMode: "All", + DeviceClassName: "nvidia-tesla", + Count: &count, + Capacity: map[string]string{ + "memory": "16Gi", + "cores": "8", + "bad": "not-a-qty", // should be skipped + }, + Selectors: []string{"zone == us-central1"}, + }, + }, + } + + kg := &K8sGenerator{ + gc: gc, + namespace: "default", + } + + got := kg.ResourceClaims() + if len(got) != 1 { + t.Fatalf("expected 1 ResourceClaimPublic, got %d", len(got)) + } + + rc := got[0] + + if rc.Name != "gpu-claim" { + t.Errorf("expected Name=gpu-claim, got %s", rc.Name) + } + if rc.Namespace != "default" { + t.Errorf("expected Namespace=default, got %s", rc.Namespace) + } + if len(rc.DeviceRequests) != 1 { + t.Fatalf("expected 1 DeviceRequest, got %d", len(rc.DeviceRequests)) + } + + dr := rc.DeviceRequests[0] + if dr.Name != "gpu1" { + t.Errorf("expected DeviceRequest.Name=gpu1, got %s", dr.Name) + } + + exact := dr.RequestsExactly + if exact == nil { + t.Fatalf("expected RequestsExactly to be non-nil") + } + if exact.AllocationMode != "All" { + t.Errorf("expected AllocationMode=All, got %s", exact.AllocationMode) + } + if exact.DeviceClassName != "nvidia-tesla" { + t.Errorf("expected DeviceClassName=nvidia-tesla, got %s", exact.DeviceClassName) + } + if exact.Count != utils.ZeroDeref(&count) { + t.Errorf("expected Count=%d, got %d", count, exact.Count) + } + + // Validate quantities + expectedQty, _ := resource.ParseQuantity("16Gi") + if !reflect.DeepEqual(exact.CapacityRequests[v1.QualifiedName("memory")], expectedQty) { + t.Errorf("expected CapacityRequests[memory]=%v, got %v", + expectedQty, exact.CapacityRequests["memory"]) + } + + // Ensure bad quantity is skipped + if _, ok := exact.CapacityRequests[v1.QualifiedName("bad")]; ok { + t.Errorf("expected bad capacity key to be skipped") + } + +} diff --git a/utils/convutils/map.go b/utils/convutils/map.go new file mode 100644 index 00000000..4bc6f274 --- /dev/null +++ b/utils/convutils/map.go @@ -0,0 +1,14 @@ +package convutils + +func ToNameMap[IN any, OUT any]( + items []IN, + getName func(IN) string, + convert func(IN) OUT, +) map[string]OUT { + result := make(map[string]OUT, len(items)) + for _, item := range items { + name := getName(item) + result[name] = convert(item) + } + return result +} diff --git a/utils/hashutils/deterministic.go b/utils/hashutils/deterministic.go new file mode 100644 index 00000000..e8494874 --- /dev/null +++ b/utils/hashutils/deterministic.go @@ -0,0 +1,87 @@ +package hashutils + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "sort" +) + +// HashJSONWithExtras computes a SHA256 hash of v plus any additional values. +// This is deterministic even for maps. +// extras can be any Go value (string, int, struct, map, etc.) +func HashDeterministicJSON(v any, extras ...any) (string, error) { + data, err := json.Marshal(v) + if err != nil { + return "", err + } + + var intermediate any + if err := json.Unmarshal(data, &intermediate); err != nil { + return "", err + } + + normalized, err := normalize(intermediate) + if err != nil { + return "", err + } + + toHash := []any{normalized} + for _, e := range extras { + data, err := json.Marshal(e) + if err != nil { + return "", err + } + var intermediate any + if err := json.Unmarshal(data, &intermediate); err != nil { + return "", err + } + norm, err := normalize(intermediate) + if err != nil { + return "", err + } + toHash = append(toHash, norm) + } + + // Marshal combined slice to deterministic JSON + finalData, err := json.Marshal(toHash) + if err != nil { + return "", err + } + + hash := sha256.Sum256(finalData) + return hex.EncodeToString(hash[:]), nil +} + +// normalize recursively sorts maps for deterministic JSON +func normalize(v any) (any, error) { + switch val := v.(type) { + case map[string]any: + sorted := make(map[string]any, len(val)) + keys := make([]string, 0, len(val)) + for k := range val { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + norm, err := normalize(val[k]) + if err != nil { + return nil, err + } + sorted[k] = norm + } + return sorted, nil + case []any: + normalizedSlice := make([]any, len(val)) + for i, elem := range val { + norm, err := normalize(elem) + if err != nil { + return nil, err + } + normalizedSlice[i] = norm + } + return normalizedSlice, nil + default: + return val, nil + } +} diff --git a/utils/hashutils/deterministic_test.go b/utils/hashutils/deterministic_test.go new file mode 100644 index 00000000..a3972f68 --- /dev/null +++ b/utils/hashutils/deterministic_test.go @@ -0,0 +1,79 @@ +package hashutils_test + +import ( + "testing" + + "github.com/kthcloud/go-deploy/utils/hashutils" + "github.com/stretchr/testify/require" +) + +func TestHashDeterministicJSON(t *testing.T) { + type Example struct { + Name string `json:"name"` + Attrs map[string]any `json:"attrs"` + } + + e1 := Example{ + Name: "Alice", + Attrs: map[string]any{ + "b": 2, + "a": 1, + }, + } + + e2 := Example{ + Name: "Alice", + Attrs: map[string]any{ + "a": 1, + "b": 2, + }, + } + + h1, err := hashutils.HashDeterministicJSON(e1) + require.NoError(t, err) + + h2, err := hashutils.HashDeterministicJSON(e2) + require.NoError(t, err) + + require.Equal(t, h1, h2, "Hashes should be equal even if map keys are reordered") + + extra := map[string]any{"version": 1} + h3, err := hashutils.HashDeterministicJSON(e1, extra) + require.NoError(t, err) + + h4, err := hashutils.HashDeterministicJSON(e2, extra) + require.NoError(t, err) + + require.Equal(t, h3, h4, "Hashes with extras should also match") + + require.NotEqual(t, h1, h3, "Hashes with extras should differ from without extras") + + nested1 := map[string]any{ + "user": map[string]any{ + "name": "Alice", + "age": 30, + }, + "tags": []any{"dev", "ops"}, + } + + nested2 := map[string]any{ + "tags": []any{"dev", "ops"}, + "user": map[string]any{ + "age": 30, + "name": "Alice", + }, + } + + h5, err := hashutils.HashDeterministicJSON(nested1) + require.NoError(t, err) + h6, err := hashutils.HashDeterministicJSON(nested2) + require.NoError(t, err) + require.Equal(t, h5, h6, "Hashes should match for nested maps with different key orders") + + h7, err := hashutils.HashDeterministicJSON(nested1, extra, "extra string") + require.NoError(t, err) + + h8, err := hashutils.HashDeterministicJSON(nested2, extra, "extra string") + require.NoError(t, err) + require.Equal(t, h7, h8, "Hashes with multiple extras should match") +} diff --git a/utils/utils.go b/utils/utils.go index c2a7463b..1a277aca 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -158,3 +158,18 @@ func Int64Ptr(i int64) *int64 { func PtrOf[T any](v T) *T { return &v } + +func ZeroDeref[T any](v *T) T { + var zero T + if v == nil { + return zero + } + return *v +} + +func ErrorStr(v error) string { + if v != nil { + return v.Error() + } + return "" +} diff --git a/utils/versionutils/version.go b/utils/versionutils/version.go new file mode 100644 index 00000000..b827c5e4 --- /dev/null +++ b/utils/versionutils/version.go @@ -0,0 +1,42 @@ +package versionutils + +import ( + "fmt" + "strconv" + + "k8s.io/apimachinery/pkg/version" +) + +type Version struct { + Major uint64 + Minor uint64 +} + +func (v Version) String() string { + return fmt.Sprintf("v%d.%d.X", v.Major, v.Minor) +} + +var DRASupportMinStable = Version{1, 34} + +func HasStableDRASupport(version *version.Info) (bool, error) { + return IsAtleast(version, DRASupportMinStable) +} + +func IsAtleast(version *version.Info, min Version) (bool, error) { + major, err := strconv.ParseUint(version.Major, 10, 64) + if err != nil { + return false, err + } + minor, err := strconv.ParseUint(version.Minor, 10, 64) + if err != nil { + return false, err + } + + if major < min.Major { + return false, nil + } else if major == min.Major && minor < min.Minor { + return false, nil + } + + return true, nil +} diff --git a/utils/versionutils/version_test.go b/utils/versionutils/version_test.go new file mode 100644 index 00000000..ce5be200 --- /dev/null +++ b/utils/versionutils/version_test.go @@ -0,0 +1,128 @@ +package versionutils + +import ( + "fmt" + "regexp" + "testing" + + "k8s.io/apimachinery/pkg/version" +) + +func TestIsAtleast(t *testing.T) { + tests := []struct { + name string + version *version.Info + minimum Version + expected bool + }{ + { + name: "Version matches exactly", + version: &version.Info{Major: "1", Minor: "34"}, + minimum: DRASupportMinStable, + expected: true, + }, + { + name: "Version is greater than minimum", + version: &version.Info{Major: "1", Minor: "35"}, + minimum: DRASupportMinStable, + expected: true, + }, + { + name: "Version is less than minimum (minor)", + version: &version.Info{Major: "1", Minor: "33"}, + minimum: DRASupportMinStable, + expected: false, + }, + { + name: "Version is less than minimum (major)", + version: &version.Info{Major: "0", Minor: "50"}, + minimum: DRASupportMinStable, + expected: false, + }, + { + name: "Version is greater than minimum (major)", + version: &version.Info{Major: "2", Minor: "0"}, + minimum: DRASupportMinStable, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _ := IsAtleast(tt.version, tt.minimum) + if got != tt.expected { + t.Errorf("IsAtleast() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestHasStableDRASupport(t *testing.T) { + testClusterVersion, err := ParseVersion("v1.34.1+rke2r1") + if err != nil { + t.Error("failed to parse testClusterVersion:", err) + return + } + tests := []struct { + name string + version *version.Info + expected bool + }{ + { + name: "Version matches minimum stable DRA support", + version: &version.Info{Major: "1", Minor: "34"}, + expected: true, + }, + { + name: "Version above minimum stable DRA support", + version: &version.Info{Major: "1", Minor: "35"}, + expected: true, + }, + { + name: "Version below minimum stable DRA support", + version: &version.Info{Major: "1", Minor: "33"}, + expected: false, + }, + { + name: "Version well below minimum stable DRA support", + version: &version.Info{Major: "0", Minor: "50"}, + expected: false, + }, + { + name: "Version above major threshold", + version: &version.Info{Major: "2", Minor: "0"}, + expected: true, + }, + { + name: "Real k8s version from test cluster", + version: testClusterVersion, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _ := HasStableDRASupport(tt.version) + if got != tt.expected { + t.Errorf("HasStableDRASupport() = %v, want %v", got, tt.expected) + } + }) + } +} + +func ParseVersion(versionStr string) (*version.Info, error) { + re := regexp.MustCompile(`^v(?P\d+)\.(?P\d+)\.(?P\d+)(\+.*)?$`) + matches := re.FindStringSubmatch(versionStr) + if len(matches) < 4 { + return nil, fmt.Errorf("invalid version string: %s", versionStr) + } + + major := matches[1] + minor := matches[2] + + return &version.Info{ + Major: major, + Minor: minor, + GitVersion: versionStr, + }, nil +}