Commit 5ddc7f7d authored by Maksim Khadkevich's avatar Maksim Khadkevich Committed by GitHub
Browse files

feat: moved compoundAI operator, APIserver, and examples (#10)

parent 14ce7e03
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/consts"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/utils"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/crds"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/models"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemas"
"github.com/rs/zerolog/log"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
type deploymentManagementService struct{}
var DeploymentManagementService = deploymentManagementService{}
type DMSConfiguration struct {
Version string `json:"version"`
Data interface{} `json:"data"`
}
type DMSCreateRequest struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
ResourceType crds.CustomResourceType `json:"type"`
Configuration interface{} `json:"configuration"`
Labels map[string]string `json:"labels"`
}
type DMSResponseStatus struct {
Status string `json:"status"`
Message string `json:"message"`
}
type DMSCreateResponse struct {
Id string `json:"id"`
Status DMSResponseStatus `json:"status"`
Configuration interface{} `json:"configuration"`
}
func (s *deploymentManagementService) Create(ctx context.Context, deploymentTarget *models.DeploymentTarget, deployOption *models.DeployOption, ownership *schemas.OwnershipSchema) (*models.DeploymentTarget, error) {
dmsHost, dmsPort, err := getDMSPortAndHost()
if err != nil {
log.Error().Msg(err.Error())
return nil, err
}
url := fmt.Sprintf("http://%s:%s/v1/deployments", dmsHost, dmsPort)
deployment, err := DeploymentService.Get(ctx, deploymentTarget.DeploymentId)
if err != nil {
log.Info().Msg("Could not find associated deployment")
return nil, err
}
defer func() {
if err != nil {
s.Delete(ctx, deploymentTarget)
}
}()
compoundNimDeployment, compoundNimRequest := s.transformToDMSRequestsV1alpha1(deployment, deploymentTarget, ownership)
body, err := sendRequest(compoundNimDeployment, url, http.MethodPost)
if err != nil {
return nil, err
}
var result DMSCreateResponse
err = json.Unmarshal(body, &result)
if err != nil {
fmt.Println("Error unmarshaling:", err)
return nil, err
}
deploymentTarget.KubeDeploymentId = result.Id
body, err = sendRequest(compoundNimRequest, url, http.MethodPost)
if err != nil {
return nil, err
}
err = json.Unmarshal(body, &result)
if err != nil {
fmt.Println("Error unmarshaling:", err)
return nil, err
}
deploymentTarget.KubeRequestId = result.Id
return deploymentTarget, nil
}
func (s *deploymentManagementService) Delete(ctx context.Context, deploymentTarget *models.DeploymentTarget) error {
dmsHost, dmsPort, err := getDMSPortAndHost()
if err != nil {
log.Error().Msg(err.Error())
return err
}
if deploymentTarget.KubeDeploymentId != "" {
urlDeployment := fmt.Sprintf("http://%s:%s/v1/deployments/%s", dmsHost, dmsPort, deploymentTarget.KubeDeploymentId)
_, err := sendRequest(nil, urlDeployment, http.MethodDelete)
if err != nil {
return err
}
}
if deploymentTarget.KubeRequestId != "" {
urlRequest := fmt.Sprintf("http://%s:%s/v1/deployments/%s", os.Getenv("DMS_HOST"), os.Getenv("DMS_PORT"), deploymentTarget.KubeRequestId)
_, err := sendRequest(nil, urlRequest, http.MethodDelete)
if err != nil {
return err
}
}
return nil
}
func (s *deploymentManagementService) transformToDMSRequestsV1alpha1(deployment *models.Deployment, deploymentTarget *models.DeploymentTarget, ownership *schemas.OwnershipSchema) (compoundNimDeployment DMSCreateRequest, compoundNimRequest DMSCreateRequest) {
translatedTag := s.translateCompoundNimVersionTagToRFC1123(deploymentTarget.CompoundNimVersionTag)
livenessProbe, readinessProbe := createProbeSpecs(deploymentTarget.Config.DeploymentOverrides)
compoundNimDeployment = DMSCreateRequest{
Name: deployment.Name,
Namespace: deployment.KubeNamespace,
ResourceType: crds.CompoundNimDeployment,
Configuration: crds.CompoundNimDeploymentConfigurationV1Alpha1{
Data: crds.CompoundNimDeploymentData{
CompoundNimVersion: translatedTag,
Resources: *deploymentTarget.Config.Resources,
ExternalServices: deploymentTarget.Config.ExternalServices,
LivenessProbe: livenessProbe,
ReadinessProbe: readinessProbe,
},
Version: crds.ApiVersion,
},
Labels: map[string]string{
consts.NgcOrganizationHeaderName: ownership.OrganizationId,
consts.NgcUserHeaderName: ownership.UserId,
},
}
compoundNimRequest = DMSCreateRequest{
Name: translatedTag,
Namespace: deployment.KubeNamespace,
ResourceType: crds.CompoundNimRequest,
Configuration: crds.CompoundNimRequestConfigurationV1Alpha1{
Data: crds.CompoundNimRequestData{
CompoundNimVersionTag: deploymentTarget.CompoundNimVersionTag,
},
Version: crds.ApiVersion,
},
Labels: map[string]string{
consts.NgcOrganizationHeaderName: ownership.OrganizationId,
consts.NgcUserHeaderName: ownership.UserId,
},
}
return
}
func createProbeSpecs(deploymentOverrides *schemas.DeploymentOverrides) (livenessProbe *corev1.Probe, readinessProbe *corev1.Probe) {
if deploymentOverrides != nil && deploymentOverrides.ColdStartTimeout != nil {
livenessProbe = &corev1.Probe{
InitialDelaySeconds: *deploymentOverrides.ColdStartTimeout,
TimeoutSeconds: 20,
FailureThreshold: 6,
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/livez",
Port: intstr.FromString(consts.CompoundNimContainerPortName),
},
},
}
readinessProbe = &corev1.Probe{
InitialDelaySeconds: *deploymentOverrides.ColdStartTimeout,
TimeoutSeconds: 5,
FailureThreshold: 12,
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/readyz",
Port: intstr.FromString(consts.CompoundNimContainerPortName),
},
},
}
}
return
}
func getDMSPortAndHost() (string, string, error) {
dmsHost, err := utils.MustGetEnv("DMS_HOST")
if err != nil {
return "", "", err
}
dmsPort, err := utils.MustGetEnv("DMS_PORT")
if err != nil {
return "", "", err
}
return dmsHost, dmsPort, nil
}
/**
* Translates a Compound NIM Version tag to a valid RFC 1123 DNS label.
*
* This function makes the following modifications to the input string:
* 1. Replaces all ":" characters with "--" because colons are not permitted in DNS labels.
* 2. If the resulting string exceeds the 63-character limit imposed by RFC 1123, it truncates
* the string to 63 characters.
*
* @param {string} tag - The original CompoundAI Nim tag that needs to be converted.
* @returns {string} - A string that complies with the RFC 1123 DNS label format.
*
* Example:
* Input: "nim:latest"
* Output: "nim--latest"
*/
func (s *deploymentManagementService) translateCompoundNimVersionTagToRFC1123(tag string) string {
translated := strings.ReplaceAll(tag, ":", "--")
// If the length exceeds 63 characters, truncate it
if len(translated) > 63 {
translated = translated[:63]
}
return translated
}
func sendRequest(payload interface{}, url string, method string) ([]byte, error) {
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %v", err)
}
req, err := http.NewRequest(method, url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("received non-OK response: %v, %s", resp.Status, body)
}
return body, nil
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package services
import (
"context"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/consts"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/database"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/models"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemas"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
type deploymentRevisionService struct{}
var DeploymentRevisionService = deploymentRevisionService{}
type CreateDeploymentRevisionOption struct {
CreatorId string
DeploymentId uint
Status schemas.DeploymentRevisionStatus
}
type UpdateDeploymentRevisionOption struct {
Status *schemas.DeploymentRevisionStatus
}
type ListDeploymentRevisionOption struct {
BaseListOption
DeploymentId *uint
DeploymentIds *[]uint
Ids *[]uint
Status *schemas.DeploymentRevisionStatus
}
func (s *deploymentRevisionService) Create(ctx context.Context, opt CreateDeploymentRevisionOption) (*models.DeploymentRevision, error) {
deploymentRevision := models.DeploymentRevision{
CreatorAssociate: models.CreatorAssociate{
UserId: opt.CreatorId,
},
DeploymentAssociate: models.DeploymentAssociate{
DeploymentId: opt.DeploymentId,
},
Status: opt.Status,
}
err := s.getDB(ctx).Create(&deploymentRevision).Error
if err != nil {
return nil, err
}
return &deploymentRevision, err
}
func (s *deploymentRevisionService) Update(ctx context.Context, deploymentRevision *models.DeploymentRevision, opt UpdateDeploymentRevisionOption) (*models.DeploymentRevision, error) {
var err error
updaters := make(map[string]interface{})
if opt.Status != nil {
updaters["status"] = *opt.Status
defer func() {
if err == nil {
deploymentRevision.Status = *opt.Status
}
}()
}
if len(updaters) == 0 {
return deploymentRevision, nil
}
log.Info().Msgf("Updating deployment revision with updaters: %+v", updaters)
err = s.getDB(ctx).Where("id = ?", deploymentRevision.ID).Updates(updaters).Error
if err != nil {
log.Error().Msgf("Failed to update deployment revision: %s", err.Error())
return nil, err
}
return deploymentRevision, err
}
func (s *deploymentRevisionService) Get(ctx context.Context, id uint) (*models.DeploymentRevision, error) {
var deploymentRevision models.DeploymentRevision
err := s.getDB(ctx).Where("id = ?", id).First(&deploymentRevision).Error
if err != nil {
log.Error().Msgf("Failed to get deployment revision by id %d: %s", id, err.Error())
return nil, err
}
if deploymentRevision.ID == 0 {
return nil, consts.ErrNotFound
}
return &deploymentRevision, nil
}
func (s *deploymentRevisionService) GetByUid(ctx context.Context, uid string) (*models.DeploymentRevision, error) {
var deploymentRevision models.DeploymentRevision
err := s.getDB(ctx).Where("uid = ?", uid).First(&deploymentRevision).Error
if err != nil {
log.Error().Msgf("Failed to get deployment revision by uid %s: %s", uid, err.Error())
return nil, err
}
if deploymentRevision.ID == 0 {
return nil, consts.ErrNotFound
}
return &deploymentRevision, nil
}
func (s *deploymentRevisionService) List(ctx context.Context, opt ListDeploymentRevisionOption) ([]*models.DeploymentRevision, uint, error) {
query := s.getDB(ctx)
if opt.DeploymentId != nil {
query = query.Where("deployment_revision.deployment_id = ?", *opt.DeploymentId)
}
if opt.DeploymentIds != nil {
query = query.Where("deployment_revision.deployment_id in (?)", *opt.DeploymentIds)
}
if opt.Status != nil {
query = query.Where("deployment_revision.status = ?", *opt.Status)
}
if opt.Ids != nil {
query = query.Where("deployment_revision.id in (?)", *opt.Ids)
}
query = query.Select("distinct(deployment_revision.*)")
var total int64
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
deployments := make([]*models.DeploymentRevision, 0)
query = opt.BindQueryWithLimit(query)
err = query.Order("deployment_revision.id DESC").Find(&deployments).Error
if err != nil {
return nil, 0, err
}
return deployments, uint(total), err
}
func (s *deploymentRevisionService) GetDeployOption(ctx context.Context, deploymentRevision *models.DeploymentRevision, force bool) (*models.DeployOption, error) {
deployOption := &models.DeployOption{
Force: force,
}
return deployOption, nil
}
func (s *deploymentRevisionService) Terminate(ctx context.Context, deploymentRevision *models.DeploymentRevision) (err error) {
deploymentTargets, _, err := DeploymentTargetService.List(ctx, ListDeploymentTargetOption{
DeploymentRevisionId: &deploymentRevision.ID,
})
if err != nil {
log.Error().Msgf("Failed to fetch deployment targets when terminating revision: %s", err.Error())
}
for _, target := range deploymentTargets {
_, err := DeploymentTargetService.Terminate(ctx, target)
if err != nil {
log.Error().Msgf("Error occurred when terminating targets for revision: %s", err.Error())
return err
}
}
status := schemas.DeploymentRevisionStatusInactive
_, err = s.Update(ctx, deploymentRevision, UpdateDeploymentRevisionOption{
Status: &status,
})
if err != nil {
log.Error().Msgf("Failed to set revision status to inactive: %s", err.Error())
return err
}
return nil
}
func (s *deploymentRevisionService) Deploy(ctx context.Context, deploymentRevision *models.DeploymentRevision, deploymentTargets []*models.DeploymentTarget, ownership *schemas.OwnershipSchema, force bool) (err error) {
_, err = DeploymentService.Get(ctx, deploymentRevision.DeploymentId)
if err != nil {
return
}
deployOption, err := s.GetDeployOption(ctx, deploymentRevision, force)
if err != nil {
return
}
if len(deploymentTargets) == 0 {
deploymentTargets, _, err = DeploymentTargetService.List(ctx, ListDeploymentTargetOption{
DeploymentRevisionId: &deploymentRevision.ID,
})
if err != nil {
return
}
}
// Can not use goroutine here because of pgx transaction bug
for _, deploymentTarget := range deploymentTargets {
_, err = DeploymentTargetService.Deploy(ctx, deploymentTarget, deployOption, ownership)
if err != nil {
return
}
}
return nil
}
func (s *deploymentRevisionService) getDB(ctx context.Context) *gorm.DB {
db := database.DatabaseUtil.GetDBSession(ctx).Model(&models.DeploymentRevision{})
return db
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package services
import (
"context"
"fmt"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/consts"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/database"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/models"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemas"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
type deploymentTargetService struct{}
var DeploymentTargetService = deploymentTargetService{}
type CreateDeploymentTargetOption struct {
CreatorId string
DeploymentId uint
DeploymentRevisionId uint
CompoundNimVersionId string
CompoundNimVersionTag string
Config *schemas.DeploymentTargetConfig
}
type UpdateDeploymentTargetOption struct {
Config **schemas.DeploymentTargetConfig
}
type ListDeploymentTargetOption struct {
BaseListOption
DeploymentRevisionStatus *schemas.DeploymentRevisionStatus
DeploymentId *uint
DeploymentIds *[]uint
DeploymentRevisionId *uint
DeploymentRevisionIds *[]uint
Type *schemas.DeploymentTargetType
}
func (s *deploymentTargetService) Create(ctx context.Context, opt CreateDeploymentTargetOption) (*models.DeploymentTarget, error) {
if opt.Config == nil {
defaultCPU := int32(80)
defaultGPU := int32(80)
defaultMinReplicas := int32(2)
defaultMaxReplicas := int32(10)
opt.Config = &schemas.DeploymentTargetConfig{
Resources: &schemas.Resources{
Requests: &schemas.ResourceItem{
CPU: "500m",
Memory: "1G",
},
Limits: &schemas.ResourceItem{
CPU: "1000m",
Memory: "2G",
},
},
HPAConf: &schemas.DeploymentTargetHPAConf{
CPU: &defaultCPU,
GPU: &defaultGPU,
MinReplicas: &defaultMinReplicas,
MaxReplicas: &defaultMaxReplicas,
},
}
}
deploymentTarget := models.DeploymentTarget{
CreatorAssociate: models.CreatorAssociate{
UserId: opt.CreatorId,
},
DeploymentAssociate: models.DeploymentAssociate{
DeploymentId: opt.DeploymentId,
},
DeploymentRevisionAssociate: models.DeploymentRevisionAssociate{
DeploymentRevisionId: opt.DeploymentRevisionId,
},
CompoundNimVersionAssociate: models.CompoundNimVersionAssociate{
CompoundNimVersionId: opt.CompoundNimVersionId,
CompoundNimVersionTag: opt.CompoundNimVersionTag,
},
Config: opt.Config,
}
err := s.getDB(ctx).Create(&deploymentTarget).Error
if err != nil {
return nil, err
}
return &deploymentTarget, err
}
func (s *deploymentTargetService) Get(ctx context.Context, id uint) (*models.DeploymentTarget, error) {
var deploymentTarget models.DeploymentTarget
err := s.getDB(ctx).Where("id = ?", id).First(&deploymentTarget).Error
if err != nil {
log.Error().Msgf("Failed to get deployment revision by id %d: %s", id, err.Error())
return nil, err
}
if deploymentTarget.ID == 0 {
return nil, consts.ErrNotFound
}
return &deploymentTarget, nil
}
func (s *deploymentTargetService) GetByUid(ctx context.Context, uid string) (*models.DeploymentTarget, error) {
var deploymentTarget models.DeploymentTarget
err := s.getDB(ctx).Where("uid = ?", uid).First(&deploymentTarget).Error
if err != nil {
log.Error().Msgf("Failed to get deployment revision by uid %s: %s", uid, err.Error())
return nil, err
}
if deploymentTarget.ID == 0 {
return nil, consts.ErrNotFound
}
return &deploymentTarget, nil
}
func (s *deploymentTargetService) List(ctx context.Context, opt ListDeploymentTargetOption) ([]*models.DeploymentTarget, uint, error) {
query := s.getDB(ctx)
if opt.DeploymentRevisionStatus != nil {
query = query.Joins("INNER JOIN deployment_revision ON deployment_revision.id = deployment_target.deployment_revision_id and deployment_revision.status = ?", *opt.DeploymentRevisionStatus)
}
if opt.DeploymentId != nil {
query = query.Where("deployment_target.deployment_id = ?", *opt.DeploymentId)
}
if opt.DeploymentRevisionId != nil {
query = query.Where("deployment_target.deployment_revision_id = ?", *opt.DeploymentRevisionId)
}
if opt.DeploymentIds != nil {
query = query.Where("deployment_target.deployment_id in (?)", *opt.DeploymentIds)
}
if opt.DeploymentRevisionIds != nil {
query = query.Where("deployment_target.deployment_revision_id in (?)", *opt.DeploymentRevisionIds)
}
if opt.Type != nil {
query = query.Where("deployment_target.type = ?", *opt.Type)
}
var total int64
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
deploymentTargets := make([]*models.DeploymentTarget, 0)
query = opt.BindQueryWithLimit(query)
err = query.Order("deployment_target.id ASC").Find(&deploymentTargets).Error
if err != nil {
return nil, 0, err
}
return deploymentTargets, uint(total), err
}
func (s *deploymentTargetService) Update(ctx context.Context, b *models.DeploymentTarget, opt UpdateDeploymentTargetOption) (*models.DeploymentTarget, error) {
var err error
updaters := make(map[string]interface{})
if opt.Config != nil {
updaters["config"] = *opt.Config
defer func() {
if err == nil {
b.Config = *opt.Config
}
}()
}
if len(updaters) == 0 {
return b, nil
}
log.Info().Msgf("Updating deployment target with updaters: %+v", updaters)
err = s.getDB(ctx).Where("id = ?", b.ID).Updates(updaters).Error
return b, err
}
func (s *deploymentTargetService) Deploy(ctx context.Context, deploymentTarget *models.DeploymentTarget, deployOption *models.DeployOption, ownership *schemas.OwnershipSchema) (*models.DeploymentTarget, error) {
err := s.getDB(ctx).Where("id = ?", deploymentTarget.ID).Save(deploymentTarget).Error
if err != nil {
deleteErr := DeploymentManagementService.Delete(ctx, deploymentTarget)
if deleteErr != nil {
log.Error().Msg("Failed to clean up kube resources for erroneous deployment")
}
err = fmt.Errorf("failed to update deploymentTarget after creating kube resources: %s", err.Error())
return nil, err
}
return deploymentTarget, nil
}
func (s *deploymentTargetService) Terminate(ctx context.Context, deploymentTarget *models.DeploymentTarget) (*models.DeploymentTarget, error) {
err := DeploymentManagementService.Delete(ctx, deploymentTarget)
if err != nil {
log.Error().Msgf("Failed to terminate kube resources for deployment target %s\n", deploymentTarget.CompoundNimVersionTag)
return nil, err
}
return deploymentTarget, nil
}
func (s *deploymentTargetService) getDB(ctx context.Context) *gorm.DB {
db := database.DatabaseUtil.GetDBSession(ctx).Model(&models.DeploymentTarget{})
return db
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package services
import (
"context"
"encoding/json"
"fmt"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/consts"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/models"
"github.com/ghodss/yaml"
apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
v1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientCmdApi "k8s.io/client-go/tools/clientcmd/api"
clientCmdLatest "k8s.io/client-go/tools/clientcmd/api/latest"
clientCmdApiV1 "k8s.io/client-go/tools/clientcmd/api/v1"
)
type k8sService struct{}
var K8sService IK8sService = &k8sService{}
func (s *k8sService) GetK8sClient(kubeConfig string) (kubernetes.Interface, error) {
var restConfig *rest.Config
var err error
if kubeConfig == "" {
restConfig, err = rest.InClusterConfig()
if err != nil {
kubeConfig :=
clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename()
restConfig, err = clientcmd.BuildConfigFromFlags("", kubeConfig)
if err != nil {
return nil, err
}
}
} else {
configV1 := clientCmdApiV1.Config{}
var jsonBytes []byte
jsonBytes, err := yaml.YAMLToJSON([]byte(kubeConfig))
if err != nil {
return nil, err
}
err = json.Unmarshal(jsonBytes, &configV1)
if err != nil {
return nil, err
}
var configObject runtime.Object
configObject, err = clientCmdLatest.Scheme.ConvertToVersion(&configV1, clientCmdApi.SchemeGroupVersion)
if err != nil {
return nil, err
}
configInternal := configObject.(*clientCmdApi.Config)
restConfig, err = clientcmd.NewDefaultClientConfig(*configInternal, &clientcmd.ConfigOverrides{
ClusterDefaults: clientCmdApi.Cluster{Server: ""},
}).ClientConfig()
if err != nil {
return nil, err
}
}
clientSet, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return nil, err
}
return clientSet, nil
}
func (s *k8sService) ListPodsByDeployment(ctx context.Context, podLister v1.PodNamespaceLister, deployment *models.Deployment) ([]*apiv1.Pod, error) {
selector, err := labels.Parse(fmt.Sprintf("%s = %s", consts.KubeLabelCompoundNimVersionDeployment, deployment.Name))
if err != nil {
return nil, err
}
return s.ListPodsBySelector(ctx, podLister, selector)
}
func (s *k8sService) ListPodsBySelector(ctx context.Context, podLister v1.PodNamespaceLister, selector labels.Selector) ([]*apiv1.Pod, error) {
pods, err := podLister.List(selector)
if err != nil {
return nil, err
}
return pods, nil
}
type IK8sService interface {
GetK8sClient(string) (kubernetes.Interface, error)
ListPodsByDeployment(context.Context, v1.PodNamespaceLister, *models.Deployment) ([]*apiv1.Pod, error)
ListPodsBySelector(context.Context, v1.PodNamespaceLister, labels.Selector) ([]*apiv1.Pod, error)
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package services
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/models"
"k8s.io/client-go/informers"
informerAppsV1 "k8s.io/client-go/informers/apps/v1"
informerCoreV1 "k8s.io/client-go/informers/core/v1"
informerNetworkingV1 "k8s.io/client-go/informers/networking/v1"
listerAppsV1 "k8s.io/client-go/listers/apps/v1"
listerCoreV1 "k8s.io/client-go/listers/core/v1"
listerNetworkingV1 "k8s.io/client-go/listers/networking/v1"
"k8s.io/client-go/tools/cache"
)
type CacheKey string
var (
informerSyncTimeout = 30 * time.Second
informerFactoryCache = make(map[CacheKey]informers.SharedInformerFactory)
informerFactoryCacheRW = &sync.RWMutex{}
)
type getSharedInformerFactoryOption struct {
cluster *models.Cluster
namespace *string
}
func getSharedInformerFactory(option *getSharedInformerFactoryOption) (informers.SharedInformerFactory, error) {
var cacheKey CacheKey
if option.namespace != nil {
cacheKey = CacheKey(fmt.Sprintf("%s:%s", option.cluster.Name, *option.namespace))
} else {
cacheKey = CacheKey(option.cluster.Name)
}
informerFactoryCacheRW.Lock()
defer informerFactoryCacheRW.Unlock()
factory, ok := informerFactoryCache[cacheKey]
if !ok {
clientset, err := K8sService.GetK8sClient(option.cluster.KubeConfig)
if err != nil {
return nil, err
}
informerOptions := make([]informers.SharedInformerOption, 0)
if option.namespace != nil {
informerOptions = append(informerOptions, informers.WithNamespace(*option.namespace))
}
factory = informers.NewSharedInformerFactoryWithOptions(clientset, 0, informerOptions...)
}
return factory, nil
}
func startAndSyncInformer(ctx context.Context, informer cache.SharedIndexInformer) (err error) {
go informer.Run(ctx.Done())
ctx_, cancel := context.WithTimeout(ctx, informerSyncTimeout)
defer cancel()
if !cache.WaitForCacheSync(ctx_.Done(), informer.HasSynced) {
err = errors.New("timed out waiting for caches to sync informer")
return err
}
return nil
}
func GetPodInformer(ctx context.Context, cluster *models.Cluster, namespace string) (informerCoreV1.PodInformer, listerCoreV1.PodNamespaceLister, error) {
factory, err := getSharedInformerFactory(&getSharedInformerFactoryOption{
cluster: cluster,
namespace: &namespace,
})
if err != nil {
return nil, nil, err
}
podInformer := factory.Core().V1().Pods()
err = startAndSyncInformer(ctx, podInformer.Informer())
if err != nil {
return nil, nil, err
}
return podInformer, podInformer.Lister().Pods(namespace), nil
}
func GetDeploymentInformer(ctx context.Context, kubeCluster *models.Cluster, namespace string) (informerAppsV1.DeploymentInformer, listerAppsV1.DeploymentNamespaceLister, error) {
factory, err := getSharedInformerFactory(&getSharedInformerFactoryOption{
cluster: kubeCluster,
namespace: &namespace,
})
if err != nil {
return nil, nil, err
}
deploymentInformer := factory.Apps().V1().Deployments()
err = startAndSyncInformer(ctx, deploymentInformer.Informer())
if err != nil {
return nil, nil, err
}
return deploymentInformer, deploymentInformer.Lister().Deployments(namespace), nil
}
func GetIngressInformer(ctx context.Context, kubeCluster *models.Cluster, namespace string) (informerNetworkingV1.IngressInformer, listerNetworkingV1.IngressNamespaceLister, error) {
factory, err := getSharedInformerFactory(&getSharedInformerFactoryOption{
cluster: kubeCluster,
namespace: &namespace,
})
if err != nil {
return nil, nil, err
}
ingressInformer := factory.Networking().V1().Ingresses()
err = startAndSyncInformer(ctx, ingressInformer.Informer())
if err != nil {
return nil, nil, err
}
return ingressInformer, ingressInformer.Lister().Ingresses(namespace), nil
}
func GetEventInformer(ctx context.Context, cluster *models.Cluster, namespace string) (informerCoreV1.EventInformer, listerCoreV1.EventNamespaceLister, error) {
factory, err := getSharedInformerFactory(&getSharedInformerFactoryOption{
cluster: cluster,
namespace: &namespace,
})
if err != nil {
return nil, nil, err
}
eventInformer := factory.Core().V1().Events()
err = startAndSyncInformer(ctx, eventInformer.Informer())
if err != nil {
return nil, nil, err
}
return eventInformer, eventInformer.Lister().Events(namespace), nil
}
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
version: "3"
services:
###
# Postgres service
# adapted from https://github.com/docker-library/docs/blob/master/postgres/README.md#-via-docker-compose-or-docker-stack-deploy
###
postgres:
image: postgres:16.2
restart: always
environment:
PGUSER: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: pgadmin
POSTGRES_DB: postgres
ports:
- "5432:5432"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-./local/data/postgres}:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 30s
timeout: 30s
retries: 3
\ No newline at end of file
## CAI Data Model
Each resource in the CAI data model is scoped either at the user or organization level. For user scoping, a user can only see resources they own. For org level scoping,
users can see any resource that belongs to their organization. The user's organization and user ID are automatically included in request headers by KAS Direct. If absent, these values default to "default".
The default scoping for most resources is user, although there are some exceptions to this. To enforce org level scoping change
the `RESOURCE_SCOPE` env var in `.env`.
### Deployments
#### 1. Cluster
All clusters are required to have org level scoping. A cluster doesn't refer to a physical K8s cluster, but rather a logical cluster. It is used to provide
some separation and organization of deployments. Currently, all deployments will run on the `compoundai` namespace. Each organization must include a cluster named default because the CLI defaults to this cluster during deployment commands.
#### 2. Deployments
Deployments are user level scoped. The `deployment` entity is used to keep track of the deployment status, K8s namespace, and other metadata. A `deployment` has
1 to n `deployment_revision`'s. Each deployment always has 1 active deployment revisions.
#### 3. Deployment Revisions
Deployment revisions are user level scoped. Each revision is owned by exactly 1 `deployment`. The `deployment_revision` is used to keep track of the history of a deployment. Each `deployment_revision` can have 1 to n `deployment_target`'s.
Each revision is marked as either active or inactive. An active deployment revision indicates that its associated targets are currently running in Kubernetes, while an inactive revision refers to terminated targets, serving primarily as historical records.
#### 4. Deployment Targets
Deployment targets are user level scoped. Each target is owned by exactly 1 `deployment` and 1 `deployment_revision`. Each `deployment_target` corresponds to a single Compound NIM instance running in Kubernetes. These entities are used when sending requests to DMS to create
any required CRDs. They keep track of important information such as the Compound NIM tag which we want to build and deploy.
### Compound NIMs
All Compound NIMs should be user scoped by default. Note: NDS v1 does not currently support user-level scoping. This functionality should be implemented during the migration from NDS v1.
##### 1. Compound NIM
This is the overarching resource that is used to keep track of a complete Compound NIM. The Compound NIM resource functions similarly to a Docker repository. For a Compound NIM tag formatted as `<name>:<tag>`, this resource represents the `<name>` portion.
A `compound_nim` resource has 1 to n `compound_nim_version`'s.
##### 1. Compound NIM Version
This is corresponds to the specific Compound NIM. Given a compound nim tag of the form `<name>:<tag>`, this resource corresponds to the `<tag>` portion. A `compound_nim_version`
resource is owned by 1 `compound_nim`.
## CAI K8s Setup
### CAI System
The CAI API Server runs on the `compoundai-system` namespace. It consists
of the `compoundai-server` and `postgresql` pods. The API server pod
has an init container that waits for Postgres to start.
There are currently two urls that can be used for the API server.
- Authenticated URL: `https://cai-api.dev.llm.ngc.nvidia.com`
- Unauthenticated URL: `https://cai-api.dev.aire.nvidia.com`
### Compound NIM Deployments
All CRDs are created in the `compoundai` namespace. These are
reconciled by the NeMo operator, and image builder jobs and deployments
are created in this namespace.
The API spec allows users to
specify the namespace their Compound NIMs are deployed to. However,
the CLI and V2 APIs default currently to `compoundai`.
Note: currently every namespace needs a secret called `compoundai-deployment-shared-env` with content similar
to this:
```yaml
apiVersion: v1
data:
BENTO_DEPLOYMENT_ALL_NAMESPACES: ZmFsc2U=
BENTO_DEPLOYMENT_NAMESPACES: Y29tcG91bmRhaQ== # replace to match current namespace
YATAI_DEPLOYMENT_NAMESPACE: Y29tcG91bmRhaQ== # replace to match current namespace
kind: Secret
metadata:
name: compoundai-deployment-shared-env
namespace: compoundai
type: Opaque
```
\ No newline at end of file
## CAI Workflow
The CAI is in charge of handling deployments and their metadata. Currently, any API calls
regarding a Compound NIM are proxied to and handled by NDS v1.
### Deployments
#### 1. Creating a deployment
When creating a deployment, we first create the `deployment` entity, followed by the `deployment_revision` (which is set to active), and then lastly we create all of the specified `deployment_target`'s.
After all of these entities are created, we then send 2 requests to DMS per `deployment_target`. These create a `CompoundAINimRequest` and `CompoundAINimDeployment` CRDs. We store the uid of each of these
resources within the `deployment_target`.
#### 2. Updating a deployment
We update any metadata in the `deployment` entity that is specified in the request. Following this we mark any active `deployment_revision`'s (should only be 1) as inactive. For any `deployment_target`'s that
belong to the old active revisions, we delete the `CompoundAINimRequest` and `CompoundAINimDeployment` CRDs which cause the deployment to be terminated on K8s. Following this, we create a new active `deployment_revision`,
all `deployment_target`'s, and all required `CompoundAINimRequest` and `CompoundAINimDeployment` CRDs.
#### 3. Terminating a deployment
We mark any active `deployment_revision`'s (should only be 1) as inactive. For any `deployment_target`'s that
belong to the old active revisions, we delete the `CompoundAINimRequest` and `CompoundAINimDeployment` CRDs which cause the deployment to be terminated on K8s.
#### 4. Deleting a deployment
If there is an active `deployment_revision` this request will error. Otherwise, we will delete all data models associated with a deployment including `deployment`, `deployment_revision`'s, and `deployment_target`'s.
\ No newline at end of file
module github.com/dynemo-ai/dynemo/deploy/compoundai/api-server
go 1.23.1
replace github.com/dynemo-ai/dynemo/deploy/compoundai/operator => ../operator
require (
github.com/ghodss/yaml v1.0.0
github.com/gin-gonic/gin v1.10.0
github.com/invopop/jsonschema v0.12.0
github.com/joho/godotenv v1.5.1
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.34.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0
k8s.io/api v0.32.0
)
require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/time v0.7.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/containerd v1.7.18 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // 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/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.12
k8s.io/apimachinery v0.32.0
k8s.io/client-go v0.32.0
)
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/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/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
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/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
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/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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
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/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
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-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.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.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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
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.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/google/go-cmp v0.5.6/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/gofuzz v1.0.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/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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
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.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
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/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 v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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/stretchr/objx v0.1.0/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo=
github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ=
github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0 h1:c51aBXT3v2HEBVarmaBnsKzvgZjC5amn0qsj8Naqi50=
github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0/go.mod h1:EWP75ogLQU4M4L8U+20mFipjV4WIR9WtlMXSB6/wiuc=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
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/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
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/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE=
k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0=
k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg=
k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
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-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
# test env
DB_USER = "postgres"
DB_PASSWORD = "pgadmin"
DB_HOST = "localhost"
DB_NAME = "test-db"
DB_PORT = "5432"
RESOURCE_SCOPE = "user"
NDS_HOST = "localhost"
NDS_PORT = "8001" # will be overwritten
\ No newline at end of file
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package integration
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"testing"
"time"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/consts"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/env"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/database"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/models"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/runtime"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemas"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemasv2"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/services"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/tests/integration/fixtures"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
const (
port = 9999
expectedStatusOkMsg = "Expected status code 200"
)
var apiServerUrl = fmt.Sprintf("http://localhost:%d", port)
var client = CompoundAIClient{
Url: apiServerUrl,
Headers: http.Header{},
}
type ApiServerSuite struct {
suite.Suite
}
func TestApiServerSuite(t *testing.T) {
suite.Run(t, new(ApiServerSuite))
}
// run once, before test suite methods
func (s *ApiServerSuite) SetupSuite() {
log.Info().Msgf("Starting suite...")
_, err := testContainers.CreatePostgresContainer()
if err != nil {
s.T().FailNow()
}
log.Info().Msgf("Created Postgres Container")
// Setup server
go func() {
// Mute all logs for this goroutine
gin.DefaultWriter = io.Discard
runtime.Runtime.StartServer(port)
}()
s.waitUntilReady()
log.Info().Msgf("API Server Ready")
services.K8sService = &fixtures.MockedK8sService{}
log.Info().Msgf("Mocked K8s Service")
}
func (s *ApiServerSuite) waitUntilReady() {
url := fmt.Sprintf("%s/healthz", apiServerUrl)
for {
resp, err := http.Get(url)
if err == nil && resp.StatusCode == http.StatusOK {
log.Info().Msg("CompoundAI API server is running")
return // Server is ready
}
log.Info().Msgf("Waiting 500ms before checking /healthz again")
time.Sleep(500 * time.Millisecond) // Wait before retrying
}
}
// run once, after test suite methods
func (s *ApiServerSuite) TearDownSuite() {
testContainers.TearDownPostgresContainer()
}
// run before every test
func (s *ApiServerSuite) SetupTest() {
client.Headers.Set(consts.NgcOrganizationHeaderName, "test-org-nvidia")
client.Headers.Set(consts.NgcUserHeaderName, "test-user-nvidia")
env.ApplicationScope = env.UserScope
}
// run after each test
func (s *ApiServerSuite) TearDownTest() {
ctx := context.Background()
db := database.DatabaseUtil.GetDBSession(ctx)
if err := db.Unscoped().Where("true").Delete(&models.Deployment{}).Error; err != nil {
s.T().Fatalf("Failed to delete records from deployment table: %v", err)
}
if err := db.Unscoped().Where("true").Delete(&models.Cluster{}).Error; err != nil {
s.T().Fatalf("Failed to delete records from cluster table: %v", err)
}
}
func (s *ApiServerSuite) TestCreateCluster() {
cluster := fixtures.DefaultCreateClusterSchema()
resp, clusterFullSchema := client.CreateCluster(s.T(), cluster)
// Verify the response status code
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Additional checks on response content (optional)
assert.Equal(s.T(), clusterFullSchema.Description, cluster.Description)
assert.Equal(s.T(), *(clusterFullSchema.KubeConfig), cluster.KubeConfig)
assert.Equal(s.T(), clusterFullSchema.Name, cluster.Name)
}
func (s *ApiServerSuite) TestGetCluster() {
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Get Cluster
resp, clusterFullSchema := client.GetCluster(s.T(), cluster.Name)
// Verify the response status code
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Additional checks on response content
assert.Equal(s.T(), clusterFullSchema.Description, cluster.Description)
assert.Equal(s.T(), *(clusterFullSchema.KubeConfig), cluster.KubeConfig)
assert.Equal(s.T(), clusterFullSchema.Name, cluster.Name)
}
func (s *ApiServerSuite) TestGetUnknownClusterFails() {
resp, _ := client.GetCluster(s.T(), "unknown")
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, "Expected status code 404")
}
func (s *ApiServerSuite) TestGetMultipleClusters() {
cluster1 := fixtures.DefaultCreateClusterSchema()
cluster1.Name = "c1"
resp, _ := client.CreateCluster(s.T(), cluster1)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
cluster2 := fixtures.DefaultCreateClusterSchema()
cluster2.Name = "c2"
resp, _ = client.CreateCluster(s.T(), cluster2)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
req := fixtures.DefaultListQuerySchema()
resp, clusterListSchema := client.GetClusterList(s.T(), req)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
for _, item := range clusterListSchema.Items {
assert.Contains(s.T(), []string{"c1", "c2"}, item.Name, expectedStatusOkMsg)
}
}
func (s *ApiServerSuite) TestUpdateCluster() {
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
req := fixtures.DefaultUpdateClusterSchema()
d := "Nemo"
kc := "KcNemo"
req.Description = &d
req.KubeConfig = &kc
resp, clusterFullSchema := client.UpdateCluster(s.T(), cluster.Name, req)
// Verify the response status code
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Additional checks on response content (optional)
assert.Equal(s.T(), clusterFullSchema.Description, *(req.Description))
assert.Equal(s.T(), *(clusterFullSchema.KubeConfig), *(req.KubeConfig))
assert.Equal(s.T(), clusterFullSchema.Name, cluster.Name)
}
func (s *ApiServerSuite) TestCreateDeployment() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
resp, deploymentSchema := client.GetDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), deployment.Name, deploymentSchema.Name)
assert.Equal(s.T(), schemas.DeploymentStatusNonDeployed, deploymentSchema.Status)
assert.Equalf(s.T(), int(1), len(deploymentSchema.LatestRevision.Targets), "expected 1 target")
assert.Equal(s.T(), deployment.Targets[0].Version, deploymentSchema.LatestRevision.Targets[0].CompoundNimVersion.Version)
}
func (s *ApiServerSuite) TestCreateDeploymentWithNDSErrorDoesNotChangeDB() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
ctx := context.Background()
d1, r1, t1, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Could not fetch deployment entities snapshot: %s", err.Error())
}
nds.Throws(true)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode)
d2, r2, t2, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Could not fetch deployment entities snapshot: %s", err.Error())
}
assert.True(s.T(), compareDeployments(d1, d2))
assert.True(s.T(), compareDeploymentRevisions(r1, r2))
assert.True(s.T(), compareDeploymentTargets(t1, t2))
}
func (s *ApiServerSuite) TestCreateDeploymentUnknownClusterFails() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ := client.CreateDeployment(s.T(), "unknown", deployment)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, "Expected status code 400")
}
func (s *ApiServerSuite) TestUpdateDeployment() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
// Create cluster
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Create deployment
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Update deployment
updateTarget := fixtures.DefaultCreateDeploymentTargetSchema()
updateTarget.Version = "2025"
updateTarget.CompoundNim = "compoundai"
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
updatedDescription := "new description"
updateDeployment.Description = &updatedDescription
updateDeployment.Targets = []*schemas.CreateDeploymentTargetSchema{
updateTarget,
}
resp, deploymentSchema := client.UpdateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, updateDeployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Validate fields
assert.Equal(s.T(), deployment.Name, deploymentSchema.Name)
assert.Equal(s.T(), schemas.DeploymentStatusNonDeployed, deploymentSchema.Status)
// Todo: once Deployment Schema is available make this test more rigorous
ctx := context.Background()
_, r1, _, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Could not fetch deployment entities snapshot: %s", err.Error())
}
assert.Equal(s.T(), 2, len(r1))
status_ := schemas.DeploymentRevisionStatusActive
activeRevisions, _, err := services.DeploymentRevisionService.List(ctx, services.ListDeploymentRevisionOption{
Status: &status_,
})
if err != nil {
s.T().Fatalf("Could not fetch revisions: %s", err.Error())
}
assert.Equal(s.T(), 1, len(activeRevisions))
assert.Equal(s.T(), deployment.Name, deploymentSchema.Name)
assert.Equal(s.T(), schemas.DeploymentStatusNonDeployed, deploymentSchema.Status)
assert.Equalf(s.T(), int(1), len(deploymentSchema.LatestRevision.Targets), "expected 1 target")
assert.Equal(s.T(), updateDeployment.Targets[0].Version, deploymentSchema.LatestRevision.Targets[0].CompoundNimVersion.Version)
}
func (s *ApiServerSuite) TestUpdateDeploymentWithNDSErrorDoesNotChangeDB() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
ctx := context.Background()
d1, r1, t1, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Could not fetch deployment entities snapshot: %s", err.Error())
}
nds.Throws(true)
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
resp, _ = client.UpdateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, updateDeployment)
assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode)
d2, r2, t2, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Could not fetch deployment entities snapshot: %s", err.Error())
}
assert.True(s.T(), compareDeployments(d1, d2))
assert.True(s.T(), compareDeploymentRevisions(r1, r2))
assert.True(s.T(), compareDeploymentTargets(t1, t2))
}
func (s *ApiServerSuite) TestUpdateDeploymentWithoutDeployment() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
// Create cluster
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Create deployment
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Update deployment
updateTarget := fixtures.DefaultCreateDeploymentTargetSchema()
updateTarget.Config.KubeResourceUid = "abc123"
updateTarget.Config.KubeResourceVersion = "alphav1"
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
updateDeployment.Targets = []*schemas.CreateDeploymentTargetSchema{
updateTarget,
}
updateDeployment.DoNotDeploy = true
ctx := context.Background()
_, r1, _, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Could not fetch deployment entities snapshot: %s", err.Error())
}
resp, deploymentSchema := client.UpdateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, updateDeployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Validate fields
assert.Equal(s.T(), deployment.Name, deploymentSchema.Name)
assert.Equal(s.T(), schemas.DeploymentStatusNonDeployed, deploymentSchema.Status)
assert.Equalf(s.T(), 1, len(deploymentSchema.LatestRevision.Targets), "More deployment targets than expected")
assert.Equal(s.T(), updateTarget.Config.KubeResourceUid, deploymentSchema.LatestRevision.Targets[0].Config.KubeResourceUid)
assert.Equal(s.T(), updateTarget.Config.KubeResourceVersion, deploymentSchema.LatestRevision.Targets[0].Config.KubeResourceVersion)
// Updating without deployment does not deactivate any deployment revision or create a new one
_, r2, _, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Could not fetch deployment entities snapshot: %s", err.Error())
}
assert.True(s.T(), compareDeploymentRevisions(r1, r2))
}
func (s *ApiServerSuite) TestUpdateDeploymentUnknownClusterFails() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
resp, _ := client.UpdateDeployment(s.T(), "unknown", "default", "unknown", updateDeployment)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, "Expected status code 404")
}
func (s *ApiServerSuite) TestUpdateDeploymentUnknownDeploymentFails() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
// Create cluster
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
resp, _ = client.UpdateDeployment(s.T(), cluster.Name, "default", "unknown", updateDeployment)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, "Expected status code 404")
}
func (s *ApiServerSuite) TestTerminateDeployment() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
ctx := context.Background()
status_ := schemas.DeploymentRevisionStatusActive
activeRevisions, _, err := services.DeploymentRevisionService.List(ctx, services.ListDeploymentRevisionOption{
Status: &status_,
})
if err != nil {
s.T().Fatalf("Could not fetch revisions: %s", err.Error())
}
assert.Equal(s.T(), 1, len(activeRevisions))
// Terminate deployment
resp, deploymentSchema := client.TerminateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
status_ = schemas.DeploymentRevisionStatusInactive
inactiveRevisions, _, err := services.DeploymentRevisionService.List(ctx, services.ListDeploymentRevisionOption{
Status: &status_,
})
if err != nil {
s.T().Fatalf("Could not fetch revisions: %s", err.Error())
}
assert.Equal(s.T(), 1, len(inactiveRevisions))
var expectedRevision *schemas.DeploymentRevisionSchema = nil
assert.Equal(s.T(), expectedRevision, deploymentSchema.LatestRevision)
}
func (s *ApiServerSuite) TestTerminateNonExistingDeployment() {
resp, _ := client.TerminateDeployment(s.T(), "nonexistent-cluster", "nonexistent-namespace", "nonexistent-deployment")
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, "Expected status code 404")
}
func (s *ApiServerSuite) TestTerminateNonIncorrectDeployment() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
resp, _ = client.TerminateDeployment(s.T(), "nonexistent-cluster", deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, "Expected status code 404")
resp, _ = client.TerminateDeployment(s.T(), cluster.Name, "nonexistent-namespace", deployment.Name)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, "Expected status code 404")
resp, _ = client.TerminateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, "nonexistent-deployment")
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, "Expected status code 404")
}
func (s *ApiServerSuite) TestDeleteDeactivatedDeployment() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Terminate the deployment
resp, _ = client.TerminateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Delete the deactivated deployment
resp, _ = client.DeleteDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Check that there are no remaining deployment entities
ctx := context.Background()
d, r, t, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Could not fetch deployment entities snapshot: %s", err.Error())
}
assert.Equal(s.T(), 0, len(d))
assert.Equal(s.T(), 0, len(r))
assert.Equal(s.T(), 0, len(t))
}
func (s *ApiServerSuite) TestDeleteActiveDeploymentFails() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Attempt to delete the active deployment
resp, _ = client.DeleteDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode)
}
func (s *ApiServerSuite) TestUpdateDeploymentWithDMSErrorDoesNotChangeDB() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
ctx := context.Background()
d1, r1, t1, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Failed to get snapshot %s", err.Error())
}
dms.Throws(true)
// Attempt to update the deployment
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
resp, _ = client.UpdateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, updateDeployment)
assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode)
d2, r2, t2, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Failed to get snapshot %s", err.Error())
}
assert.True(s.T(), compareDeployments(d1, d2))
assert.True(s.T(), compareDeploymentRevisions(r1, r2))
assert.True(s.T(), compareDeploymentTargets(t1, t2))
}
func (s *ApiServerSuite) TestTerminateDeploymentWithDMSErrorDoesNotChangeDB() {
dms := fixtures.CreateMockDMSServer(s.T()) // Does not throw error initially
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
ctx := context.Background()
d1, r1, t1, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Failed to get snapshot %s", err.Error())
}
dms.Throws(true)
// Attempt to terminate the deployment
resp, _ = client.TerminateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode)
// Verify DB state remains unchanged
d2, r2, t2, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Failed to get snapshot %s", err.Error())
}
assert.True(s.T(), compareDeployments(d1, d2))
assert.True(s.T(), compareDeploymentRevisions(r1, r2))
assert.True(s.T(), compareDeploymentTargets(t1, t2))
}
func (s *ApiServerSuite) TestGetDeploymentRevisions() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, deploymentSchema := client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
resp, deploymentRevisionSchema := client.GetDeploymentRevision(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, deploymentSchema.LatestRevision.Uid)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), deploymentSchema.LatestRevision.Uid, deploymentRevisionSchema.Uid)
lr, err := json.Marshal(deploymentSchema.LatestRevision)
if err != nil {
s.T().Fatalf("%s", err.Error())
}
ar, err := json.Marshal(deploymentSchema.LatestRevision)
if err != nil {
s.T().Fatalf("%s", err.Error())
}
assert.Equal(s.T(), lr, ar)
}
func (s *ApiServerSuite) TestGetDeploymentRevisionsDifferentDeployments() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
d1 := fixtures.DefaultCreateDeploymentSchema()
d1.Name = "dep1"
resp, ds1 := client.CreateDeployment(s.T(), cluster.Name, d1)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
d2 := fixtures.DefaultCreateDeploymentSchema()
d2.Name = "dep2"
resp, ds2 := client.CreateDeployment(s.T(), cluster.Name, d2)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
resp, drs1 := client.GetDeploymentRevision(s.T(), cluster.Name, d1.KubeNamespace, d1.Name, ds1.LatestRevision.Uid)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
resp, drs2 := client.GetDeploymentRevision(s.T(), cluster.Name, d1.KubeNamespace, d1.Name, ds2.LatestRevision.Uid)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.NotEqual(s.T(), drs2.Uid, drs1.Uid)
}
func (s *ApiServerSuite) TestGetDeploymentRevisionsList() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
resp, _ = client.UpdateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, updateDeployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
query := schemas.ListQuerySchema{
Count: 10,
}
resp, deploymentRevisionsListSchema := client.GetDeploymentRevisionList(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, query)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), 2, len(deploymentRevisionsListSchema.Items))
}
func (s *ApiServerSuite) TestOrganizationLevelScopeForClusterWhenCreatingDeployment() {
env.ApplicationScope = env.OrganizationScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcOrganizationHeaderName, "some-other-org")
deployment2 := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment2)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode)
}
func (s *ApiServerSuite) TestOrganizationLevelScopeForClusterWhenUpdatingDeployment() {
env.ApplicationScope = env.OrganizationScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcOrganizationHeaderName, "some-other-org")
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
resp, _ = client.UpdateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, updateDeployment)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode)
}
func (s *ApiServerSuite) TestOrganizationLevelScopeForClusterWhenTerminatingDeployment() {
env.ApplicationScope = env.OrganizationScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Terminate deployment
client.Headers.Set(consts.NgcOrganizationHeaderName, "some-other-org")
resp, _ = client.TerminateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, expectedStatusOkMsg)
}
func (s *ApiServerSuite) TestOrganizationLevelScopeForClusterWhenDeletingDeployment() {
env.ApplicationScope = env.OrganizationScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Terminate the deployment
resp, _ = client.TerminateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Delete the deactivated deployment
client.Headers.Set(consts.NgcOrganizationHeaderName, "some-other-org")
resp, _ = client.DeleteDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, expectedStatusOkMsg)
}
func (s *ApiServerSuite) TestOrganizationLevelScopeForClusterWhenListingDeploymentRevisions() {
env.ApplicationScope = env.OrganizationScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
resp, _ = client.UpdateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, updateDeployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcOrganizationHeaderName, "some-other-org")
query := schemas.ListQuerySchema{}
resp, _ = client.GetDeploymentRevisionList(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, query)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, expectedStatusOkMsg)
}
func (s *ApiServerSuite) TestOrganizationLevelScopeForGetClusterDeployments() {
env.ApplicationScope = env.OrganizationScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
client.Headers.Set(consts.NgcOrganizationHeaderName, "org-1")
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcOrganizationHeaderName, "org-2")
cluster = fixtures.DefaultCreateClusterSchema()
resp, _ = client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment = fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcOrganizationHeaderName, "org-1")
resp, deploymentSchemas := client.GetClusterDeploymentList(s.T(), cluster.Name, schemas.ListQuerySchema{Count: 10})
assert.Equalf(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), 1, len(deploymentSchemas.Items))
}
func (s *ApiServerSuite) TestOrganizationLevelScopeForGetDeployments() {
env.ApplicationScope = env.OrganizationScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
client.Headers.Set(consts.NgcOrganizationHeaderName, "org-1")
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcOrganizationHeaderName, "org-2")
cluster = fixtures.DefaultCreateClusterSchema()
resp, _ = client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment = fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcOrganizationHeaderName, "org-1")
resp, deploymentSchemas := client.GetDeploymentList(s.T(), schemas.ListQuerySchema{Count: 10})
assert.Equalf(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), 1, len(deploymentSchemas.Items))
}
func (s *ApiServerSuite) TestUserLevelScopeForClusterWhenCreatingDeployment() {
env.ApplicationScope = env.UserScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
deployment.Name = "dep1"
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcUserHeaderName, "some-other-user")
deployment2 := fixtures.DefaultCreateDeploymentSchema()
deployment2.Name = "dep2"
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment2)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
resp, deploymentSchemas := client.GetClusterDeploymentList(s.T(), cluster.Name, schemas.ListQuerySchema{Count: 10})
assert.Equalf(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), 1, len(deploymentSchemas.Items))
}
func (s *ApiServerSuite) TestUserLevelScopeForClusterWhenUpdatingDeployment() {
env.ApplicationScope = env.UserScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcUserHeaderName, "some-other-user")
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
resp, _ = client.UpdateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, updateDeployment)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode)
}
func (s *ApiServerSuite) TestUserLevelScopeForClusterWhenTerminatingDeployment() {
env.ApplicationScope = env.UserScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcUserHeaderName, "some-other-user")
resp, _ = client.TerminateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, expectedStatusOkMsg)
}
func (s *ApiServerSuite) TestUserLevelScopeForClusterWhenDeletingDeployment() {
env.ApplicationScope = env.UserScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
resp, _ = client.TerminateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcUserHeaderName, "some-other-user")
resp, _ = client.DeleteDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, expectedStatusOkMsg)
}
func (s *ApiServerSuite) TestUserLevelScopeForClusterWhenListingDeploymentRevisions() {
env.ApplicationScope = env.UserScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
updateDeployment := fixtures.DefaultUpdateDeploymentSchema()
resp, _ = client.UpdateDeployment(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, updateDeployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcUserHeaderName, "some-other-user")
query := schemas.ListQuerySchema{}
resp, _ = client.GetDeploymentRevisionList(s.T(), cluster.Name, deployment.KubeNamespace, deployment.Name, query)
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, expectedStatusOkMsg)
}
func (s *ApiServerSuite) TestUserLevelScopeForGetClusterDeployments() {
env.ApplicationScope = env.UserScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
client.Headers.Set(consts.NgcUserHeaderName, "user-1")
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
deployment.Name = "dep1"
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcUserHeaderName, "user-2")
deployment = fixtures.DefaultCreateDeploymentSchema()
deployment.Name = "dep2"
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcUserHeaderName, "user-1")
resp, deploymentSchemas := client.GetClusterDeploymentList(s.T(), cluster.Name, schemas.ListQuerySchema{Count: 10})
assert.Equalf(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), 1, len(deploymentSchemas.Items))
}
func (s *ApiServerSuite) TestUserLevelScopeForGetDeployments() {
env.ApplicationScope = env.UserScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
client.Headers.Set(consts.NgcUserHeaderName, "user-1")
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchema()
deployment.Name = "dep1"
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcUserHeaderName, "user-2")
deployment = fixtures.DefaultCreateDeploymentSchema()
deployment.Name = "dep2"
resp, _ = client.CreateDeployment(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcUserHeaderName, "user-1")
resp, deploymentSchemas := client.GetDeploymentList(s.T(), schemas.ListQuerySchema{Count: 10})
assert.Equalf(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), 1, len(deploymentSchemas.Items))
}
func (s *ApiServerSuite) TestOrganizationLevelScopeForListClusters() {
env.ApplicationScope = env.OrganizationScope
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
client.Headers.Set(consts.NgcOrganizationHeaderName, "some-other-org")
resp, _ = client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
req := fixtures.DefaultListQuerySchema()
resp, clusterListSchema := client.GetClusterList(s.T(), req)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), 1, len(clusterListSchema.Items))
}
// V2 API Tests
func (s *ApiServerSuite) TestCreateDeploymentV2() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchemaV2()
resp, _ = client.CreateDeploymentV2(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
resp, deploymentSchema := client.GetDeploymentV2(s.T(), cluster.Name, deployment.Name)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), deployment.Name, deploymentSchema.Name)
assert.Equal(s.T(), schemas.DeploymentStatusNonDeployed, deploymentSchema.Status)
assert.Equalf(s.T(), int(1), len(deploymentSchema.LatestRevision.Targets), "expected 1 target")
assert.Equal(s.T(), deployment.Services["default-service"].ConfigOverrides.Resources, *deploymentSchema.LatestRevision.Targets[0].Config.Resources)
}
func (s *ApiServerSuite) TestUpdateDeploymentV2() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
// Create cluster
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Create deployment
deployment := fixtures.DefaultCreateDeploymentSchemaV2()
resp, _ = client.CreateDeploymentV2(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Update deployment
resourceItem := &schemas.ResourceItem{
CPU: "123m",
GPU: "2",
Memory: "5Gi",
}
updateService := fixtures.DefaultServiceSpec()
updateService.ConfigOverrides.Resources.Limits = resourceItem
updateService.ConfigOverrides.Resources.Requests = resourceItem
updateDeployment := fixtures.DefaultUpdateDeploymentSchemaV2()
updateDeployment.CompoundNim = "new:654321"
updateDeployment.DeploymentConfigSchema.Services = map[string]schemasv2.ServiceSpec{
"new-service": updateService,
}
resp, deploymentSchema := client.UpdateDeploymentV2(s.T(), cluster.Name, deployment.Name, updateDeployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
assert.Equal(s.T(), deployment.Name, deploymentSchema.Name)
assert.Equal(s.T(), schemas.DeploymentStatusNonDeployed, deploymentSchema.Status)
assert.Equalf(s.T(), int(1), len(deploymentSchema.LatestRevision.Targets), "expected 1 target")
assert.Equal(s.T(), updateDeployment.Services["new-service"].ConfigOverrides.Resources, *deploymentSchema.LatestRevision.Targets[0].Config.Resources)
}
func (s *ApiServerSuite) TestTerminateDeploymentV2() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchemaV2()
resp, _ = client.CreateDeploymentV2(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
ctx := context.Background()
status_ := schemas.DeploymentRevisionStatusActive
activeRevisions, _, err := services.DeploymentRevisionService.List(ctx, services.ListDeploymentRevisionOption{
Status: &status_,
})
if err != nil {
s.T().Fatalf("Could not fetch revisions: %s", err.Error())
}
assert.Equal(s.T(), 1, len(activeRevisions))
// Terminate deployment
resp, deploymentSchema := client.TerminateDeploymentV2(s.T(), cluster.Name, deployment.Name)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
status_ = schemas.DeploymentRevisionStatusInactive
inactiveRevisions, _, err := services.DeploymentRevisionService.List(ctx, services.ListDeploymentRevisionOption{
Status: &status_,
})
if err != nil {
s.T().Fatalf("Could not fetch revisions: %s", err.Error())
}
assert.Equal(s.T(), 1, len(inactiveRevisions))
var expectedRevision *schemas.DeploymentRevisionSchema = nil
assert.Equal(s.T(), expectedRevision, deploymentSchema.LatestRevision)
}
func (s *ApiServerSuite) TestDeleteDeactivatedDeploymentV2() {
dms := fixtures.CreateMockDMSServer(s.T())
defer dms.Close()
nds := fixtures.CreateMockNDSServer(s.T())
defer nds.Close()
cluster := fixtures.DefaultCreateClusterSchema()
resp, _ := client.CreateCluster(s.T(), cluster)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
deployment := fixtures.DefaultCreateDeploymentSchemaV2()
resp, _ = client.CreateDeploymentV2(s.T(), cluster.Name, deployment)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Terminate the deployment
resp, _ = client.TerminateDeploymentV2(s.T(), cluster.Name, deployment.Name)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Delete the deactivated deployment
resp, _ = client.DeleteDeploymentV2(s.T(), cluster.Name, deployment.Name)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, expectedStatusOkMsg)
// Check that there are no remaining deployment entities
ctx := context.Background()
d, r, t, err := getDeploymentEntitiesSnapshot(ctx)
if err != nil {
s.T().Fatalf("Could not fetch deployment entities snapshot: %s", err.Error())
}
assert.Equal(s.T(), 0, len(d))
assert.Equal(s.T(), 0, len(r))
assert.Equal(s.T(), 0, len(t))
}
func compareDeployments(slice1, slice2 []*models.Deployment) bool {
// Check if lengths are equal
if len(slice1) != len(slice2) {
return false
}
// Compare each element using reflect.DeepEqual
for i := range slice1 {
if !reflect.DeepEqual(slice1[i], slice2[i]) {
log.Info().Msgf("Expected deployment: %+v", slice1[i])
log.Info().Msgf("Actual deployment: %+v", slice2[i])
return false
}
}
return true
}
func compareDeploymentRevisions(slice1, slice2 []*models.DeploymentRevision) bool {
// Check if lengths are equal
if len(slice1) != len(slice2) {
return false
}
// Compare each element using reflect.DeepEqual
for i := range slice1 {
if !reflect.DeepEqual(slice1[i], slice2[i]) {
log.Info().Msgf("Expected revision: %+v", slice1[i])
log.Info().Msgf("Actual revision: %+v", slice2[i])
return false
}
}
return true
}
func compareDeploymentTargets(slice1, slice2 []*models.DeploymentTarget) bool {
// Check if lengths are equal
if len(slice1) != len(slice2) {
return false
}
// Compare each element using reflect.DeepEqual
for i := range slice1 {
if !reflect.DeepEqual(slice1[i], slice2[i]) {
log.Info().Msgf("Expected target: %+v", slice1[i])
log.Info().Msgf("Actual target: %+v", slice2[i])
return false
}
}
return true
}
func getDeploymentEntitiesSnapshot(ctx context.Context) ([]*models.Deployment, []*models.DeploymentRevision, []*models.DeploymentTarget, error) {
deployments, _, err := services.DeploymentService.List(ctx, services.ListDeploymentOption{})
if err != nil {
return nil, nil, nil, err
}
revisions, _, err := services.DeploymentRevisionService.List(ctx, services.ListDeploymentRevisionOption{})
if err != nil {
return nil, nil, nil, err
}
targets, _, err := services.DeploymentTargetService.List(ctx, services.ListDeploymentTargetOption{})
if err != nil {
return nil, nil, nil, err
}
return deployments, revisions, targets, nil
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package integration
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"testing"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemas"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemasv2"
)
/**
This file exposes a series of helper functions to utilize the CompoundAI API server
**/
type CompoundAIClient struct {
Url string
Headers http.Header
}
func (c *CompoundAIClient) CreateCluster(t *testing.T, s schemas.CreateClusterSchema) (*http.Response, *schemas.ClusterFullSchema) {
// Marshal the request body
body, err := json.Marshal(s)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
// Create a new HTTP request
req, err := http.NewRequest(http.MethodPost, c.Url+"/api/v1/clusters", bytes.NewBuffer(body))
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
// Set headers from the client
req.Header.Set("Content-Type", "application/json")
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value) // Use Add to support multiple values for the same header key
}
}
// Use http.Client to execute the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to create cluster: %v", err)
}
defer resp.Body.Close()
// Read the response
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
// Unmarshal the response into the schema
var clusterFullSchema schemas.ClusterFullSchema
if err = json.Unmarshal(respBody, &clusterFullSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &clusterFullSchema
}
func (c *CompoundAIClient) UpdateCluster(t *testing.T, name string, s schemas.UpdateClusterSchema) (*http.Response, *schemas.ClusterFullSchema) {
body, err := json.Marshal(s)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
// Create the PATCH request with JSON data
req, err := http.NewRequest(http.MethodPatch, c.Url+"/api/v1/clusters/"+name, bytes.NewBuffer(body))
if err != nil {
t.Fatalf("Failed create update request %s", err.Error())
}
// Set the appropriate headers
req.Header.Set("Content-Type", "application/json")
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value) // Use Add to support multiple values for the same header key
}
}
// Create an HTTP client and send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to update cluster %s", err.Error())
}
defer resp.Body.Close()
// Read the response
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var clusterFullSchema schemas.ClusterFullSchema
if err = json.Unmarshal(respBody, &clusterFullSchema); err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
return resp, &clusterFullSchema
}
func encodeListQuerySchema(query schemas.ListQuerySchema) string {
params := url.Values{}
params.Set("start", strconv.FormatUint(uint64(query.Start), 10))
params.Set("count", strconv.FormatUint(uint64(query.Count), 10))
if query.Search != nil {
params.Set("search", *query.Search)
}
params.Set("q", query.Q)
return params.Encode()
}
func (c *CompoundAIClient) GetCluster(t *testing.T, name string) (*http.Response, *schemas.ClusterFullSchema) {
req, err := http.NewRequest(http.MethodGet, c.Url+"/api/v1/clusters/"+name, nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
// Set headers from the client
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to get cluster: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return resp, nil
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var clusterFullSchema schemas.ClusterFullSchema
if err = json.Unmarshal(respBody, &clusterFullSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &clusterFullSchema
}
func (c *CompoundAIClient) GetClusterList(t *testing.T, s schemas.ListQuerySchema) (*http.Response, *schemas.ClusterListSchema) {
form := encodeListQuerySchema(s)
req, err := http.NewRequest(http.MethodGet, c.Url+"/api/v1/clusters?"+form, nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
// Set headers from the client
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to get cluster list: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var clusterListSchema schemas.ClusterListSchema
if err = json.Unmarshal(respBody, &clusterListSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &clusterListSchema
}
func (c *CompoundAIClient) CreateDeployment(t *testing.T, clusterName string, s schemas.CreateDeploymentSchema) (*http.Response, *schemas.DeploymentSchema) {
body, err := json.Marshal(s)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
req, err := http.NewRequest(http.MethodPost, c.Url+"/api/v1/clusters/"+clusterName+"/deployments", bytes.NewBuffer(body))
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to create deployment: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var deploymentSchema schemas.DeploymentSchema
if err = json.Unmarshal(respBody, &deploymentSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &deploymentSchema
}
func (c *CompoundAIClient) UpdateDeployment(t *testing.T, clusterName, namespace string, deploymentName string, s schemas.UpdateDeploymentSchema) (*http.Response, *schemas.DeploymentSchema) {
body, err := json.Marshal(s)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
// Create the PATCH request with JSON data
req, err := http.NewRequest(http.MethodPatch, c.Url+"/api/v1/clusters/"+clusterName+"/namespaces/"+namespace+"/deployments/"+deploymentName, bytes.NewBuffer(body))
if err != nil {
t.Fatalf("Failed create update request %s", err.Error())
}
// Set the appropriate headers
req.Header.Set("Content-Type", "application/json")
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
// Create an HTTP client and send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to update deployment %s", err.Error())
}
defer resp.Body.Close()
// Read the response
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var deploymentSchema schemas.DeploymentSchema
if err = json.Unmarshal(respBody, &deploymentSchema); err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
return resp, &deploymentSchema
}
func (c *CompoundAIClient) GetDeployment(t *testing.T, clusterName, namespace, deploymentName string) (*http.Response, *schemas.DeploymentSchema) {
req, err := http.NewRequest(http.MethodGet, c.Url+"/api/v1/clusters/"+clusterName+"/namespaces/"+namespace+"/deployments/"+deploymentName, nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to get deployment: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return resp, nil
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var deploymentSchema schemas.DeploymentSchema
if err = json.Unmarshal(respBody, &deploymentSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &deploymentSchema
}
func (c *CompoundAIClient) SyncDeploymentStatus(t *testing.T, clusterName, namespace, deploymentName string) (*http.Response, any) {
req, err := http.NewRequest(http.MethodPost, c.Url+"/api/v1/clusters/"+clusterName+"/namespaces/"+namespace+"/deployments/"+deploymentName+"/sync_status", nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to sync deployment status: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var statusResponse any
if err = json.Unmarshal(respBody, &statusResponse); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, statusResponse
}
func (c *CompoundAIClient) TerminateDeployment(t *testing.T, clusterName, namespace, deploymentName string) (*http.Response, *schemas.DeploymentSchema) {
req, err := http.NewRequest(http.MethodPost, c.Url+"/api/v1/clusters/"+clusterName+"/namespaces/"+namespace+"/deployments/"+deploymentName+"/terminate", nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to terminate deployment: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var terminateResponse schemas.DeploymentSchema
if err = json.Unmarshal(respBody, &terminateResponse); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &terminateResponse
}
func (c *CompoundAIClient) DeleteDeployment(t *testing.T, clusterName, namespace, deploymentName string) (*http.Response, any) {
req, err := http.NewRequest(http.MethodDelete, c.Url+"/api/v1/clusters/"+clusterName+"/namespaces/"+namespace+"/deployments/"+deploymentName, nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to delete deployment: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var deleteResponse any
if err = json.Unmarshal(respBody, &deleteResponse); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, deleteResponse
}
func (c *CompoundAIClient) GetDeploymentList(t *testing.T, queryParams schemas.ListQuerySchema) (*http.Response, *schemas.DeploymentListSchema) {
query := url.Values{}
query.Add("q", queryParams.Q)
if queryParams.Start > 0 {
query.Add("start", fmt.Sprintf("%d", queryParams.Start))
}
if queryParams.Count > 0 {
query.Add("count", fmt.Sprintf("%d", queryParams.Count))
}
if queryParams.Search != nil {
query.Add("search", *queryParams.Search)
}
req, err := http.NewRequest(http.MethodGet, c.Url+"/api/v1/deployments?"+query.Encode(), nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to get deployment revisions list: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return resp, nil
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
var deploymentListSchema schemas.DeploymentListSchema
if err = json.Unmarshal(respBody, &deploymentListSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &deploymentListSchema
}
func (c *CompoundAIClient) GetClusterDeploymentList(t *testing.T, clusterName string, queryParams schemas.ListQuerySchema) (*http.Response, *schemas.DeploymentListSchema) {
query := url.Values{}
query.Add("q", queryParams.Q)
if queryParams.Start > 0 {
query.Add("start", fmt.Sprintf("%d", queryParams.Start))
}
if queryParams.Count > 0 {
query.Add("count", fmt.Sprintf("%d", queryParams.Count))
}
if queryParams.Search != nil {
query.Add("search", *queryParams.Search)
}
req, err := http.NewRequest(http.MethodGet, c.Url+"/api/v1/clusters/"+clusterName+"/deployments?"+query.Encode(), nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to get deployment revisions list: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return resp, nil
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
var deploymentListSchema schemas.DeploymentListSchema
if err = json.Unmarshal(respBody, &deploymentListSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &deploymentListSchema
}
func (c *CompoundAIClient) GetDeploymentRevision(t *testing.T, clusterName, namespace, deploymentName, revisionUid string) (*http.Response, *schemas.DeploymentRevisionSchema) {
req, err := http.NewRequest(http.MethodGet, c.Url+"/api/v1/clusters/"+clusterName+"/namespaces/"+namespace+"/deployments/"+deploymentName+"/revisions/"+revisionUid, nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to get deployment revision: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return resp, nil
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
var deploymentRevisionSchema schemas.DeploymentRevisionSchema
if err = json.Unmarshal(respBody, &deploymentRevisionSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &deploymentRevisionSchema
}
func (c *CompoundAIClient) GetDeploymentRevisionList(t *testing.T, clusterName, namespace, deploymentName string, queryParams schemas.ListQuerySchema) (*http.Response, *schemas.DeploymentRevisionListSchema) {
query := url.Values{}
query.Add("q", queryParams.Q)
if queryParams.Start > 0 {
query.Add("start", fmt.Sprintf("%d", queryParams.Start))
}
if queryParams.Count > 0 {
query.Add("count", fmt.Sprintf("%d", queryParams.Count))
}
if queryParams.Search != nil {
query.Add("search", *queryParams.Search)
}
req, err := http.NewRequest(http.MethodGet, c.Url+"/api/v1/clusters/"+clusterName+"/namespaces/"+namespace+"/deployments/"+deploymentName+"/revisions?"+query.Encode(), nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to get deployment revisions list: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return resp, nil
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
var deploymentRevisionListSchema schemas.DeploymentRevisionListSchema
if err = json.Unmarshal(respBody, &deploymentRevisionListSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &deploymentRevisionListSchema
}
// V2 Client Functions
func (c *CompoundAIClient) CreateDeploymentV2(t *testing.T, clusterName string, s schemasv2.CreateDeploymentSchema) (*http.Response, *schemasv2.DeploymentSchema) {
body, err := json.Marshal(s)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
req, err := http.NewRequest(http.MethodPost, c.Url+"/api/v2/deployments?cluster="+clusterName, bytes.NewBuffer(body))
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to create deployment: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var deploymentSchema schemasv2.DeploymentSchema
if err = json.Unmarshal(respBody, &deploymentSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &deploymentSchema
}
func (c *CompoundAIClient) UpdateDeploymentV2(t *testing.T, clusterName, deploymentName string, s schemasv2.UpdateDeploymentSchema) (*http.Response, *schemasv2.DeploymentSchema) {
body, err := json.Marshal(s)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
// Create the PATCH request with JSON data
req, err := http.NewRequest(http.MethodPut, c.Url+"/api/v2/deployments/"+deploymentName+"?cluster="+clusterName, bytes.NewBuffer(body))
if err != nil {
t.Fatalf("Failed create update request %s", err.Error())
}
// Set the appropriate headers
req.Header.Set("Content-Type", "application/json")
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
// Create an HTTP client and send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to update deployment %s", err.Error())
}
defer resp.Body.Close()
// Read the response
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var deploymentSchema schemasv2.DeploymentSchema
if err = json.Unmarshal(respBody, &deploymentSchema); err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
return resp, &deploymentSchema
}
func (c *CompoundAIClient) GetDeploymentV2(t *testing.T, clusterName, deploymentName string) (*http.Response, *schemasv2.DeploymentSchema) {
req, err := http.NewRequest(http.MethodGet, c.Url+"/api/v2/deployments/"+deploymentName+"?cluster="+clusterName, nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to get deployment: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return resp, nil
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var deploymentSchema schemasv2.DeploymentSchema
if err = json.Unmarshal(respBody, &deploymentSchema); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &deploymentSchema
}
func (c *CompoundAIClient) TerminateDeploymentV2(t *testing.T, clusterName, deploymentName string) (*http.Response, *schemasv2.DeploymentSchema) {
req, err := http.NewRequest(http.MethodPost, c.Url+"/api/v2/deployments/"+deploymentName+"/terminate"+"?cluster="+clusterName, nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to terminate deployment: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var terminateResponse schemasv2.DeploymentSchema
if err = json.Unmarshal(respBody, &terminateResponse); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, &terminateResponse
}
func (c *CompoundAIClient) DeleteDeploymentV2(t *testing.T, clusterName, deploymentName string) (*http.Response, *schemasv2.DeploymentSchema) {
req, err := http.NewRequest(http.MethodDelete, c.Url+"/api/v2/deployments/"+deploymentName+"?cluster="+clusterName, nil)
if err != nil {
t.Fatalf("Failed to create HTTP request: %v", err)
}
for key, values := range c.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to delete deployment: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return resp, nil
}
var deleteResponse *schemasv2.DeploymentSchema
if err = json.Unmarshal(respBody, &deleteResponse); err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
return resp, deleteResponse
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package integration
import (
"context"
"os"
"time"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/database"
"github.com/joho/godotenv"
"github.com/rs/zerolog/log"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
)
type TestContainers struct {
postgres *postgres.PostgresContainer
}
var testContainers = TestContainers{}
const (
postgresImage = "postgres:16.2"
)
func (c *TestContainers) CreatePostgresContainer() (*postgres.PostgresContainer, error) {
err := godotenv.Load()
if err != nil {
log.Error().Msgf("Failed to load env vars for during integration test setup: %s", err.Error())
}
ctx := context.Background()
postgres, err := postgres.Run(ctx,
postgresImage,
postgres.WithDatabase(os.Getenv(database.DB_NAME)),
postgres.WithUsername(os.Getenv(database.DB_USER)),
postgres.WithPassword(os.Getenv(database.DB_PASSWORD)),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).WithStartupTimeout(10*time.Second)),
)
if err != nil {
log.Error().Msgf("Could not create Postgres container: %s", err.Error())
return nil, err
}
containerPort, err := postgres.MappedPort(ctx, "5432")
if err != nil {
log.Error().Msgf("Could not get mapped port: %s", err.Error())
return nil, err
}
os.Setenv(database.DB_PORT, containerPort.Port())
log.Info().Msgf("Started postgres container %+v on port %s", postgres, containerPort.Port())
c.postgres = postgres
return postgres, nil
}
func (c *TestContainers) TearDownPostgresContainer() error {
log.Info().Msgf("terminating postgres container")
ctx := context.Background()
err := c.postgres.Terminate(ctx)
if err != nil {
log.Error().Msgf("Failed to terminate test Postgres container: %s", err.Error())
return err
}
return nil
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fixtures
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/services"
)
type MockDMSServer struct {
server *httptest.Server
throws *bool
}
func (s *MockDMSServer) Close() {
s.server.Close()
}
func (s *MockDMSServer) Throws(throws bool) {
s.throws = &throws
}
func CreateMockDMSServer(t *testing.T) *MockDMSServer {
throws := false
mockServer := MockDMSServer{}
mockServer.throws = &throws
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if *mockServer.throws {
w.WriteHeader(http.StatusBadRequest)
return
}
response := services.DMSCreateResponse{
Id: "abc123",
Status: services.DMSResponseStatus{
Status: "success",
Message: "DMS resource created successfully",
},
Configuration: map[string]string{
"setting1": "value1",
"setting2": "value2",
"setting3": "value3",
},
}
jsonResponse, err := json.Marshal(response)
if err != nil {
t.Fatalf("Failed to marshal JSON %s", err.Error())
}
w.WriteHeader(http.StatusOK)
w.Write(jsonResponse)
}))
idx := strings.LastIndex(server.URL, ":")
os.Setenv("DMS_HOST", "localhost")
os.Setenv("DMS_PORT", server.URL[idx+1:])
mockServer.server = server
return &mockServer
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fixtures
import (
"context"
"fmt"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/consts"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/models"
"github.com/rs/zerolog/log"
apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/listers/core/v1"
)
type MockedK8sService struct{}
func (s *MockedK8sService) GetK8sClient(kubeConfig string) (kubernetes.Interface, error) {
log.Info().Msgf("Using fake client.")
return fake.NewClientset(), nil
}
func (s *MockedK8sService) ListPodsByDeployment(ctx context.Context, podLister v1.PodNamespaceLister, deployment *models.Deployment) ([]*apiv1.Pod, error) {
log.Info().Msgf("Faking list by deployment")
selector, err := labels.Parse(fmt.Sprintf("%s = %s", consts.KubeLabelCompoundNimVersionDeployment, deployment.Name))
if err != nil {
return nil, err
}
return s.ListPodsBySelector(ctx, podLister, selector)
}
func (s *MockedK8sService) ListPodsBySelector(ctx context.Context, podLister v1.PodNamespaceLister, selector labels.Selector) ([]*apiv1.Pod, error) {
log.Info().Msgf("Faking list by selector")
pods, err := podLister.List(selector)
if err != nil {
return nil, err
}
return pods, nil
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fixtures
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/env"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemas"
)
type MockNDSServer struct {
server *httptest.Server
throws *bool
}
func (s *MockNDSServer) Close() {
s.server.Close()
}
func (s *MockNDSServer) Throws(throws bool) {
s.throws = &throws
}
func CreateMockNDSServer(t *testing.T) *MockNDSServer {
throws := false
mockServer := MockNDSServer{}
mockServer.throws = &throws
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if *mockServer.throws {
w.WriteHeader(http.StatusBadRequest)
return
}
urlParts := strings.Split(r.URL.String(), "/")
n := len(urlParts)
response := schemas.CompoundNimVersionSchema{
ResourceSchema: schemas.ResourceSchema{
BaseSchema: schemas.BaseSchema{
Uid: "123456",
},
Name: "benquadinaros",
},
Version: urlParts[n-1],
}
jsonResponse, err := json.Marshal(response)
if err != nil {
t.Fatalf("Failed to marshal JSON %s", err.Error())
}
w.WriteHeader(http.StatusOK)
w.Write(jsonResponse)
}))
idx := strings.LastIndex(server.URL, ":")
os.Setenv("NDS_HOST", "localhost")
os.Setenv("NDS_PORT", server.URL[idx+1:])
env.NdsHostBase = fmt.Sprintf("localhost:%s", server.URL[idx+1:]) // This var is cached and must be set to the new value
mockServer.server = server
return &mockServer
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fixtures
import "github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemas"
func DefaultCreateClusterSchema() schemas.CreateClusterSchema {
return schemas.CreateClusterSchema{
Description: "description",
KubeConfig: "",
Name: "default",
}
}
func DefaultUpdateClusterSchema() schemas.UpdateClusterSchema {
d := "description"
kc := "kubeconfig"
return schemas.UpdateClusterSchema{
Description: &d,
KubeConfig: &kc,
}
}
func DefaultListQuerySchema() schemas.ListQuerySchema {
return schemas.ListQuerySchema{
Start: 0,
Count: 20,
Search: nil,
}
}
// DefaultCreateDeploymentSchema generates a default CreateDeploymentSchema
func DefaultCreateDeploymentSchema() schemas.CreateDeploymentSchema {
return schemas.CreateDeploymentSchema{
Name: "default-deployment",
KubeNamespace: "default-namespace",
UpdateDeploymentSchema: DefaultUpdateDeploymentSchema(),
}
}
// DefaultUpdateDeploymentSchema generates a default UpdateDeploymentSchema
func DefaultUpdateDeploymentSchema() schemas.UpdateDeploymentSchema {
description := "default deployment"
return schemas.UpdateDeploymentSchema{
Targets: []*schemas.CreateDeploymentTargetSchema{
DefaultCreateDeploymentTargetSchema(),
},
Description: &description,
DoNotDeploy: false,
}
}
// DefaultCreateDeploymentTargetSchema generates a default CreateDeploymentTargetSchema
func DefaultCreateDeploymentTargetSchema() *schemas.CreateDeploymentTargetSchema {
return &schemas.CreateDeploymentTargetSchema{
CompoundNim: "default-compound-nim",
Version: "default-version",
Config: DefaultDeploymentTargetConfig(),
}
}
// DefaultDeploymentTargetConfig generates a default DeploymentTargetConfig
func DefaultDeploymentTargetConfig() *schemas.DeploymentTargetConfig {
return &schemas.DeploymentTargetConfig{
KubeResourceUid: "default-uid",
KubeResourceVersion: "v1",
Resources: DefaultResources(),
HPAConf: DefaultDeploymentTargetHPAConf(),
DeploymentStrategy: DefaultDeploymentStrategy(),
}
}
// DefaultResources generates a default Resources struct
func DefaultResources() *schemas.Resources {
return &schemas.Resources{
Requests: &schemas.ResourceItem{
CPU: "500m",
Memory: "1Gi",
},
Limits: &schemas.ResourceItem{
CPU: "1",
Memory: "2Gi",
},
}
}
// DefaultDeploymentTargetHPAConf generates a default DeploymentTargetHPAConf
func DefaultDeploymentTargetHPAConf() *schemas.DeploymentTargetHPAConf {
qps := int64(1000)
return &schemas.DeploymentTargetHPAConf{
CPU: nil,
GPU: nil,
Memory: nil,
QPS: &qps,
MinReplicas: int32Ptr(1),
MaxReplicas: int32Ptr(5),
}
}
// DefaultDeploymentStrategy generates a default DeploymentStrategy
func DefaultDeploymentStrategy() *schemas.DeploymentStrategy {
strategy := schemas.DeploymentStrategyRollingUpdate
return &strategy
}
// Helper function to return a pointer to an int32
func int32Ptr(i int32) *int32 {
return &i
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fixtures
import (
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemas"
"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemasv2"
)
// DefaultScalingSpec generates a default ScalingSpec
func DefaultScalingSpec() schemasv2.ScalingSpec {
return schemasv2.ScalingSpec{
MinReplicas: 1,
MaxReplicas: 10,
}
}
// DefaultConfigOverridesSpec generates a default ConfigOverridesSpec
func DefaultConfigOverridesSpec() schemasv2.ConfigOverridesSpec {
return schemasv2.ConfigOverridesSpec{
Resources: *DefaultResources(),
}
}
// DefaultServiceSpec generates a default ServiceSpec
func DefaultServiceSpec() schemasv2.ServiceSpec {
return schemasv2.ServiceSpec{
Scaling: DefaultScalingSpec(),
ConfigOverrides: DefaultConfigOverridesSpec(),
ExternalServices: map[string]schemas.ExternalService{},
ColdStartTimeout: nil,
}
}
// DefaultDeploymentConfigSchema generates a default DeploymentConfigSchema
func DefaultDeploymentConfigSchema() schemasv2.DeploymentConfigSchema {
return schemasv2.DeploymentConfigSchema{
AccessAuthorization: true,
Envs: map[string]string{
"ENV_VAR": "value",
},
Secrets: map[string]string{
"SECRET_KEY": "secret-value",
},
Services: map[string]schemasv2.ServiceSpec{
"default-service": DefaultServiceSpec(),
},
}
}
// DefaultUpdateDeploymentSchema generates a default UpdateDeploymentSchema
func DefaultUpdateDeploymentSchemaV2() schemasv2.UpdateDeploymentSchema {
return schemasv2.UpdateDeploymentSchema{
DeploymentConfigSchema: DefaultDeploymentConfigSchema(),
CompoundNim: "nvidia:123456",
}
}
// DefaultCreateDeploymentSchema generates a default CreateDeploymentSchema
func DefaultCreateDeploymentSchemaV2() schemasv2.CreateDeploymentSchema {
return schemasv2.CreateDeploymentSchema{
Name: "default-deployment",
UpdateDeploymentSchema: DefaultUpdateDeploymentSchemaV2(),
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment