Compare commits

...

24 Commits

Author SHA1 Message Date
Josiah Clumont 2c1f3c27d5 fix(grpc): upgrade to v1.79.3 to mitigate CVE-2026-33186 [C9S-55] (#2089) 2026-03-20 10:33:49 +13:00
Chaim Lev-Ari b468160606 fix(stacks): disabled edit button while submit [BE-12681] (#2093) 2026-03-19 16:09:16 +02:00
Josiah Clumont a42e96b650 chore: bump 2.39.0 to 2.39.1 (#2085) 2026-03-19 08:21:14 +13:00
Chaim Lev-Ari 5a19f66a37 fix(stacks): validate stacks with env vars [BE-12689] (#2051) 2026-03-17 20:05:16 -03:00
andres-portainer b271026188 fix(otel): upgrade to v1.42.0 BE-12724 (#2071) 2026-03-17 13:02:50 -03:00
LP B d168e3c912 fix(api/uac): panic on external stacks UAC eval (#2074) 2026-03-17 16:00:39 +01:00
andres-portainer 0b6ebd70e0 fix(go): upgrade Go to v1.25.8 to mitigate CVEs BE-12721 (#2066) 2026-03-17 09:36:48 -03:00
andres-portainer 127e03552a fix(docker): upgrade Docker binary to v29.3.0 to mitigate CVE-2025-68121 BE-12720 (#2063) 2026-03-16 17:52:30 -03:00
Chaim Lev-Ari f2bdfc6eff fix(kube/app): enable edit button for regular apps [BE-12690] (#2040) 2026-03-15 11:48:38 +02:00
Cara Ryan 5db67faa00 chore(kubernetes): Upgrade k8s deps to 0.35 [C9S-32] (#2042) 2026-03-13 15:44:38 +13:00
andres-portainer f9dcfcb435 fix(GO-2026-4550): upgrade circl to v1.6.3 BE-12694 (#2012) 2026-03-06 14:29:28 -03:00
LP B 1d1bb526d0 fix(app/container): query env registries instead of system registries (#1997) 2026-03-06 14:18:09 -03:00
LP B c8fe8ba4fd fix(app): paginate nested tables (#1999) 2026-03-06 14:17:34 -03:00
andres-portainer d3692a5a5f fix(GO-2026-4473): upgrade go-git to v5.17.0 BE-12693 (#2009) 2026-03-06 11:24:08 -03:00
LP B 3407811c28 fix(app/stack): virtual grouping in EnvSelector for non admins (#2002) 2026-03-06 15:00:28 +01:00
andres-portainer b71db0d1f1 fix(GO-2026-4394): upgrade opentelemetry to v1.41.0 BE-12692 (#2004) 2026-03-06 08:31:22 -03:00
LP B 5e5e85ff3a fix(api/custom_template): validate UAC when retrieving custom template file (#1981) 2026-03-04 13:22:09 +01:00
RHCowan 65d82e12ee fix(policy) avoid URL length limit when adding environments to large groups [R8S-893] (#1970) (#1972) 2026-02-27 12:00:54 +13:00
Steven Kang d9e730e0a5 fix(kubernetes): local exec to fall back to SPDY - release 2.39 [R8S-873] (#1947) 2026-02-25 15:46:16 +13:00
Ali 21eb20b35e fix(environment-groups): allow bulk selecting environments on create and edit [r8s-872] (#1956)
Merging because the failed system tests are related to helm and not environment groups
2026-02-24 17:53:06 +13:00
Steven Kang f85a7ea24c fix(environment): collapsing More options breaking the style for podman - release 2.39 [R8S-874] (#1943) 2026-02-24 10:11:50 +13:00
Oscar Zhou 6aacb61c87 fix(stack): avoid removing running service if stack deployment fails [BE-12542] (#1941) 2026-02-24 08:41:53 +13:00
andres-portainer bb2c75ba93 fix(policies): fixes for async edge R8S-661 (#1934) 2026-02-20 17:45:38 -03:00
Steven Kang 16536c8a71 feat(environment): reorder options - release 2.39 [R8S-524] (#1924) 2026-02-20 14:58:05 +13:00
61 changed files with 1923 additions and 1072 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ see also:
## Package Manager ## Package Manager
- **PNPM** 10+ (for frontend) - **PNPM** 10+ (for frontend)
- **Go** 1.25.7 (for backend) - **Go** 1.25.8 (for backend)
## Build Commands ## Build Commands
@@ -613,7 +613,7 @@
"RequiredPasswordLength": 12 "RequiredPasswordLength": 12
}, },
"KubeconfigExpiry": "0", "KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.39.0", "KubectlShellImage": "portainer/kubectl-shell:2.39.1",
"LDAPSettings": { "LDAPSettings": {
"AnonymousMode": true, "AnonymousMode": true,
"AutoCreateUsers": true, "AutoCreateUsers": true,
@@ -942,7 +942,7 @@
} }
], ],
"version": { "version": {
"VERSION": "{\"SchemaVersion\":\"2.39.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" "VERSION": "{\"SchemaVersion\":\"2.39.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}, },
"webhooks": null "webhooks": null
} }
@@ -2,8 +2,14 @@ package customtemplates
import ( import (
"net/http" "net/http"
"strconv"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/slicesx"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response" "github.com/portainer/portainer/pkg/libhttp/response"
@@ -33,11 +39,46 @@ func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Reques
return httperror.BadRequest("Invalid custom template identifier route variable", err) return httperror.BadRequest("Invalid custom template identifier route variable", err)
} }
customTemplate, err := handler.DataStore.CustomTemplate().Read(portainer.CustomTemplateID(customTemplateID)) var customTemplate *portainer.CustomTemplate
if handler.DataStore.IsErrObjectNotFound(err) { if err := handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
return httperror.NotFound("Unable to find a custom template with the specified identifier inside the database", err) var err error
} else if err != nil { customTemplate, err = tx.CustomTemplate().Read(portainer.CustomTemplateID(customTemplateID))
return httperror.InternalServerError("Unable to find a custom template with the specified identifier inside the database", err) if tx.IsErrObjectNotFound(err) {
return httperror.NotFound("Unable to find a custom template with the specified identifier inside the database", err)
} else if err != nil {
return httperror.InternalServerError("Unable to find a custom template with the specified identifier inside the database", err)
}
resourceControl, err := tx.ResourceControl().ResourceControlByResourceIDAndType(strconv.Itoa(customTemplateID), portainer.CustomTemplateResourceControl)
if err != nil {
return httperror.InternalServerError("Unable to retrieve a resource control associated to the custom template", err)
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return httperror.InternalServerError("Unable to retrieve user info from request context", err)
}
canEdit := userCanEditTemplate(customTemplate, securityContext)
hasAccess := false
if resourceControl != nil {
customTemplate.ResourceControl = resourceControl
teamIDs := slicesx.Map(securityContext.UserMemberships, func(m portainer.TeamMembership) portainer.TeamID {
return m.TeamID
})
hasAccess = authorization.UserCanAccessResource(securityContext.UserID, teamIDs, resourceControl)
}
if canEdit || hasAccess {
return nil
}
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
}); err != nil {
return response.TxErrorResponse(err)
} }
entryPath := customTemplate.EntryPoint entryPath := customTemplate.EntryPoint
@@ -0,0 +1,115 @@
package customtemplates
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/require"
)
func TestCustomTemplateFile(t *testing.T) {
_, ds := datastore.MustNewTestStore(t, true, false)
require.NotNil(t, ds)
fs, err := filesystem.NewService(t.TempDir(), t.TempDir())
require.NoError(t, err)
templateContent := "some template content"
templateEntrypoint := "entrypoint"
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
require.NoError(t, tx.User().Create(&portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}))
require.NoError(t, tx.User().Create(&portainer.User{ID: 2, Username: "std2", Role: portainer.StandardUserRole}))
require.NoError(t, tx.User().Create(&portainer.User{ID: 3, Username: "std3", Role: portainer.StandardUserRole}))
require.NoError(t, tx.User().Create(&portainer.User{ID: 4, Username: "std4", Role: portainer.StandardUserRole}))
require.NoError(t, tx.Endpoint().Create(&portainer.Endpoint{ID: 1,
UserAccessPolicies: portainer.UserAccessPolicies{
2: portainer.AccessPolicy{RoleID: 0},
3: portainer.AccessPolicy{RoleID: 0},
}}))
require.NoError(t, tx.Team().Create(&portainer.Team{ID: 1}))
require.NoError(t, tx.TeamMembership().Create(&portainer.TeamMembership{ID: 1, UserID: 3, TeamID: 1, Role: portainer.TeamMember}))
// template 1
path, err := fs.StoreCustomTemplateFileFromBytes("1", templateEntrypoint, []byte(templateContent))
require.NoError(t, err)
require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{ID: 1, EntryPoint: templateEntrypoint, ProjectPath: path}))
// template 2
path, err = fs.StoreCustomTemplateFileFromBytes("2", templateEntrypoint, []byte(templateContent))
require.NoError(t, err)
require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{ID: 2, EntryPoint: templateEntrypoint, ProjectPath: path}))
require.NoError(t, tx.ResourceControl().Create(&portainer.ResourceControl{ID: 1, ResourceID: "2", Type: portainer.CustomTemplateResourceControl,
UserAccesses: []portainer.UserResourceAccess{{UserID: 2}},
TeamAccesses: []portainer.TeamResourceAccess{{TeamID: 1}},
}))
return nil
}))
handler := NewHandler(testhelpers.NewTestRequestBouncer(), ds, fs, nil)
test := func(templateID string, restrictedContext *security.RestrictedRequestContext) (*httptest.ResponseRecorder, *httperror.HandlerError) {
r := httptest.NewRequest(http.MethodGet, "/custom_templates/"+templateID+"/file", nil)
r = mux.SetURLVars(r, map[string]string{"id": templateID})
ctx := security.StoreRestrictedRequestContext(r, restrictedContext)
r = r.WithContext(ctx)
rr := httptest.NewRecorder()
return rr, handler.customTemplateFile(rr, r)
}
t.Run("unknown id should get not found error", func(t *testing.T) {
_, r := test("0", &security.RestrictedRequestContext{UserID: 1})
require.NotNil(t, r)
require.Equal(t, http.StatusNotFound, r.StatusCode)
})
t.Run("admin should access adminonly template", func(t *testing.T) {
rr, r := test("1", &security.RestrictedRequestContext{UserID: 1, IsAdmin: true})
require.Nil(t, r)
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
var res struct{ FileContent string }
require.NoError(t, json.NewDecoder(rr.Body).Decode(&res))
require.Equal(t, templateContent, res.FileContent)
})
t.Run("std should not access adminonly template", func(t *testing.T) {
_, r := test("1", &security.RestrictedRequestContext{UserID: 2})
require.NotNil(t, r)
require.Equal(t, http.StatusForbidden, r.StatusCode)
})
t.Run("std should access template via direct user access", func(t *testing.T) {
rr, r := test("2", &security.RestrictedRequestContext{UserID: 2})
require.Nil(t, r)
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
var res struct{ FileContent string }
require.NoError(t, json.NewDecoder(rr.Body).Decode(&res))
require.Equal(t, templateContent, res.FileContent)
})
t.Run("std should access template via team access", func(t *testing.T) {
rr, r := test("2", &security.RestrictedRequestContext{UserID: 3, UserMemberships: []portainer.TeamMembership{{ID: 1, UserID: 3, TeamID: 1}}})
require.Nil(t, r)
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
var res struct{ FileContent string }
require.NoError(t, json.NewDecoder(rr.Body).Decode(&res))
require.Equal(t, templateContent, res.FileContent)
})
t.Run("std should not access template without access", func(t *testing.T) {
_, r := test("2", &security.RestrictedRequestContext{UserID: 4})
require.NotNil(t, r)
require.Equal(t, http.StatusForbidden, r.StatusCode)
})
}
@@ -38,7 +38,7 @@ func (handler *Handler) customTemplateInspect(w http.ResponseWriter, r *http.Req
var customTemplate *portainer.CustomTemplate var customTemplate *portainer.CustomTemplate
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error { err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
customTemplate, err = tx.CustomTemplate().Read(portainer.CustomTemplateID(customTemplateID)) customTemplate, err = tx.CustomTemplate().Read(portainer.CustomTemplateID(customTemplateID))
if handler.DataStore.IsErrObjectNotFound(err) { if tx.IsErrObjectNotFound(err) {
return httperror.NotFound("Unable to find a custom template with the specified identifier inside the database", err) return httperror.NotFound("Unable to find a custom template with the specified identifier inside the database", err)
} else if err != nil { } else if err != nil {
return httperror.InternalServerError("Unable to find a custom template with the specified identifier inside the database", err) return httperror.InternalServerError("Unable to find a custom template with the specified identifier inside the database", err)
@@ -9,6 +9,7 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore" "github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers" "github.com/portainer/portainer/api/internal/testhelpers"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
@@ -20,6 +21,9 @@ func TestInspectHandler(t *testing.T) {
_, ds := datastore.MustNewTestStore(t, true, false) _, ds := datastore.MustNewTestStore(t, true, false)
require.NotNil(t, ds) require.NotNil(t, ds)
fs, err := filesystem.NewService(t.TempDir(), t.TempDir())
require.NoError(t, err)
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
require.NoError(t, tx.User().Create(&portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole})) require.NoError(t, tx.User().Create(&portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}))
require.NoError(t, tx.User().Create(&portainer.User{ID: 2, Username: "std2", Role: portainer.StandardUserRole})) require.NoError(t, tx.User().Create(&portainer.User{ID: 2, Username: "std2", Role: portainer.StandardUserRole}))
@@ -42,7 +46,7 @@ func TestInspectHandler(t *testing.T) {
return nil return nil
})) }))
handler := NewHandler(testhelpers.NewTestRequestBouncer(), ds, &TestFileService{}, nil) handler := NewHandler(testhelpers.NewTestRequestBouncer(), ds, fs, nil)
test := func(templateID string, restrictedContext *security.RestrictedRequestContext) (*httptest.ResponseRecorder, *httperror.HandlerError) { test := func(templateID string, restrictedContext *security.RestrictedRequestContext) (*httptest.ResponseRecorder, *httperror.HandlerError) {
r := httptest.NewRequest(http.MethodGet, "/custom_templates/"+templateID, nil) r := httptest.NewRequest(http.MethodGet, "/custom_templates/"+templateID, nil)
+7 -1
View File
@@ -19,6 +19,7 @@ type StackViewModel struct {
Name string Name string
IsExternal bool IsExternal bool
Type portainer.StackType Type portainer.StackType
Labels map[string]string
} }
// GetDockerStacks retrieves all the stacks associated to a specific environment filtered by the user's access. // GetDockerStacks retrieves all the stacks associated to a specific environment filtered by the user's access.
@@ -56,6 +57,7 @@ func GetDockerStacks(tx dataservices.DataStoreTx, securityContext *security.Rest
Name: name, Name: name,
IsExternal: true, IsExternal: true,
Type: portainer.DockerComposeStack, Type: portainer.DockerComposeStack,
Labels: container.Labels,
} }
} }
} }
@@ -68,6 +70,7 @@ func GetDockerStacks(tx dataservices.DataStoreTx, securityContext *security.Rest
Name: name, Name: name,
IsExternal: true, IsExternal: true,
Type: portainer.DockerSwarmStack, Type: portainer.DockerSwarmStack,
Labels: service.Spec.Labels,
} }
} }
} }
@@ -79,7 +82,10 @@ func GetDockerStacks(tx dataservices.DataStoreTx, securityContext *security.Rest
return uac.FilterByResourceControl(stacksList, user, securityContext.UserMemberships, return uac.FilterByResourceControl(stacksList, user, securityContext.UserMemberships,
func(item StackViewModel) (*portainer.ResourceControl, error) { func(item StackViewModel) (*portainer.ResourceControl, error) {
return uac.StackResourceControlGetter(tx, environmentID)(*item.InternalStack) if item.InternalStack != nil {
return uac.StackResourceControlGetter(tx, environmentID)(*item.InternalStack)
}
return uac.ExternalStackResourceControlGetter(tx, environmentID)(uac.ExternalStack{Labels: item.Labels})
}, },
) )
} }
@@ -8,7 +8,7 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore" "github.com/portainer/portainer/api/datastore"
dockerconsts "github.com/portainer/portainer/api/docker/consts" "github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -28,12 +28,13 @@ func TestHandler_getDockerStacks(t *testing.T) {
containers := []types.Container{ containers := []types.Container{
{ {
Labels: map[string]string{ Labels: map[string]string{
dockerconsts.ComposeStackNameLabel: "stack1", consts.ComposeStackNameLabel: "stack1",
}, },
}, },
{ {
Labels: map[string]string{ Labels: map[string]string{
dockerconsts.ComposeStackNameLabel: "stack2", consts.ComposeStackNameLabel: "stack2",
"io.portainer.accesscontrol.public": "true",
}, },
}, },
} }
@@ -43,7 +44,7 @@ func TestHandler_getDockerStacks(t *testing.T) {
Spec: swarm.ServiceSpec{ Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{ Annotations: swarm.Annotations{
Labels: map[string]string{ Labels: map[string]string{
dockerconsts.SwarmStackNameLabel: "stack3", consts.SwarmStackNameLabel: "stack3",
}, },
}, },
}, },
@@ -65,14 +66,16 @@ func TestHandler_getDockerStacks(t *testing.T) {
is.NoError(tx.Stack().Create(&stack1)) is.NoError(tx.Stack().Create(&stack1))
is.NoError(tx.Stack().Create(&portainer.Stack{ is.NoError(tx.Stack().Create(&portainer.Stack{
ID: 2, ID: 2,
Name: "stack2", Name: "stack2", // stack 2 on env 2
EndpointID: 2, EndpointID: 2,
Type: portainer.DockerSwarmStack, Type: portainer.DockerSwarmStack,
})) }))
is.NoError(tx.User().Create(&portainer.User{ID: 1, Role: portainer.AdministratorRole})) is.NoError(tx.User().Create(&portainer.User{ID: 1, Role: portainer.AdministratorRole}))
is.NoError(tx.User().Create(&portainer.User{ID: 2, Role: portainer.StandardUserRole}))
return nil return nil
})) }))
// testing admin user
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error { is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
stacksList, err := GetDockerStacks(tx, &security.RestrictedRequestContext{ stacksList, err := GetDockerStacks(tx, &security.RestrictedRequestContext{
IsAdmin: true, IsAdmin: true,
@@ -93,11 +96,43 @@ func TestHandler_getDockerStacks(t *testing.T) {
Name: "stack2", Name: "stack2",
IsExternal: true, IsExternal: true,
Type: portainer.DockerComposeStack, Type: portainer.DockerComposeStack,
Labels: map[string]string{
consts.ComposeStackNameLabel: "stack2",
"io.portainer.accesscontrol.public": "true",
},
}, },
{ {
Name: "stack3", Name: "stack3",
IsExternal: true, IsExternal: true,
Type: portainer.DockerSwarmStack, Type: portainer.DockerSwarmStack,
Labels: map[string]string{
consts.SwarmStackNameLabel: "stack3",
},
},
}
assert.ElementsMatch(t, expectedStacks, stacksList)
return nil
}))
// testing standard user
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
stacksList, err := GetDockerStacks(tx, &security.RestrictedRequestContext{
IsAdmin: false,
UserID: 2,
}, environment.ID, containers, services)
require.NoError(t, err)
assert.Len(t, stacksList, 1)
expectedStacks := []StackViewModel{
{
Name: "stack2",
IsExternal: true,
Type: portainer.DockerComposeStack,
Labels: map[string]string{
consts.ComposeStackNameLabel: "stack2",
"io.portainer.accesscontrol.public": "true",
},
}, },
} }
@@ -39,6 +39,7 @@ const (
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags" // @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
// @param endpointIds query []int false "will return only these environments(endpoints)" // @param endpointIds query []int false "will return only these environments(endpoints)"
// @param excludeIds query []int false "will exclude these environments(endpoints)" // @param excludeIds query []int false "will exclude these environments(endpoints)"
// @param excludeGroupIds query []int false "will exclude environments(endpoints) belonging to these endpoint groups"
// @param provisioned query bool false "If true, will return environment(endpoint) that were provisioned" // @param provisioned query bool false "If true, will return environment(endpoint) that were provisioned"
// @param agentVersions query []string false "will return only environments with on of these agent versions" // @param agentVersions query []string false "will return only environments with on of these agent versions"
// @param edgeAsync query bool false "if exists true show only edge async agents, false show only standard edge agents. if missing, will show both types (relevant only for edge agents)" // @param edgeAsync query bool false "if exists true show only edge async agents, false show only standard edge agents. if missing, will show both types (relevant only for edge agents)"
+13
View File
@@ -38,6 +38,7 @@ type EnvironmentsQuery struct {
edgeStackId portainer.EdgeStackID edgeStackId portainer.EdgeStackID
edgeStackStatus *portainer.EdgeStackStatusType edgeStackStatus *portainer.EdgeStackStatusType
excludeIds []portainer.EndpointID excludeIds []portainer.EndpointID
excludeGroupIds []portainer.EndpointGroupID
edgeGroupIds []portainer.EdgeGroupID edgeGroupIds []portainer.EdgeGroupID
excludeEdgeGroupIds []portainer.EdgeGroupID excludeEdgeGroupIds []portainer.EdgeGroupID
} }
@@ -80,6 +81,11 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
return EnvironmentsQuery{}, err return EnvironmentsQuery{}, err
} }
excludeGroupIDs, err := getNumberArrayQueryParameter[portainer.EndpointGroupID](r, "excludeGroupIds")
if err != nil {
return EnvironmentsQuery{}, err
}
edgeGroupIDs, err := getNumberArrayQueryParameter[portainer.EdgeGroupID](r, "edgeGroupIds") edgeGroupIDs, err := getNumberArrayQueryParameter[portainer.EdgeGroupID](r, "edgeGroupIds")
if err != nil { if err != nil {
return EnvironmentsQuery{}, err return EnvironmentsQuery{}, err
@@ -119,6 +125,7 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
tagIds: tagIDs, tagIds: tagIDs,
endpointIds: endpointIDs, endpointIds: endpointIDs,
excludeIds: excludeIDs, excludeIds: excludeIDs,
excludeGroupIds: excludeGroupIDs,
tagsPartialMatch: tagsPartialMatch, tagsPartialMatch: tagsPartialMatch,
groupIds: groupIDs, groupIds: groupIDs,
status: status, status: status,
@@ -157,6 +164,12 @@ func (handler *Handler) filterEndpointsByQuery(
}) })
} }
if len(query.excludeGroupIds) > 0 {
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
return !slices.Contains(query.excludeGroupIds, endpoint.GroupID)
})
}
if len(query.groupIds) > 0 { if len(query.groupIds) > 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds) filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
} }
+40
View File
@@ -151,6 +151,46 @@ func Test_Filter_excludeIDs(t *testing.T) {
runTests(tests, t, handler, environments) runTests(tests, t, handler, environments)
} }
func Test_Filter_excludeGroupIDs(t *testing.T) {
groupA := portainer.EndpointGroupID(10)
groupB := portainer.EndpointGroupID(20)
groupC := portainer.EndpointGroupID(30)
endpoints := []portainer.Endpoint{
{ID: 1, GroupID: groupA, Type: portainer.DockerEnvironment},
{ID: 2, GroupID: groupA, Type: portainer.DockerEnvironment},
{ID: 3, GroupID: groupB, Type: portainer.DockerEnvironment},
{ID: 4, GroupID: groupB, Type: portainer.DockerEnvironment},
{ID: 5, GroupID: groupC, Type: portainer.DockerEnvironment},
}
handler := setupFilterTest(t, endpoints)
tests := []filterTest{
{
title: "should exclude endpoints in groupA",
expected: []portainer.EndpointID{3, 4, 5},
query: EnvironmentsQuery{
excludeGroupIds: []portainer.EndpointGroupID{groupA},
},
},
{
title: "should exclude endpoints in groupA and groupB",
expected: []portainer.EndpointID{5},
query: EnvironmentsQuery{
excludeGroupIds: []portainer.EndpointGroupID{groupA, groupB},
},
},
{
title: "should return all endpoints when excludeGroupIds is empty",
expected: []portainer.EndpointID{1, 2, 3, 4, 5},
query: EnvironmentsQuery{},
},
}
runTests(tests, t, handler, endpoints)
}
func BenchmarkFilterEndpointsBySearchCriteria_PartialMatch(b *testing.B) { func BenchmarkFilterEndpointsBySearchCriteria_PartialMatch(b *testing.B) {
n := 10000 n := 10000
+1 -1
View File
@@ -81,7 +81,7 @@ type Handler struct {
} }
// @title PortainerCE API // @title PortainerCE API
// @version 2.39.0 // @version 2.39.1
// @description.markdown api-description.md // @description.markdown api-description.md
// @termsOfService // @termsOfService
+26 -10
View File
@@ -3,8 +3,10 @@ package cli
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@@ -55,29 +57,43 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
TTY: true, TTY: true,
}, scheme.ParameterCodec) }, scheme.ParameterCodec)
streamOpts := remotecommand.StreamOptions{
Stdin: stdin,
Stdout: stdout,
Tty: true,
}
// Try WebSocket executor first, fall back to SPDY if it fails
exec, err := remotecommand.NewWebSocketExecutorForProtocols( exec, err := remotecommand.NewWebSocketExecutorForProtocols(
config, config,
"GET", // WebSocket uses GET for the upgrade request "GET", // WebSocket uses GET for the upgrade request
req.URL().String(), req.URL().String(),
channelProtocolList..., channelProtocolList...,
) )
if err != nil { if err == nil {
exec, err = remotecommand.NewSPDYExecutor(config, "POST", req.URL()) err = exec.StreamWithContext(context.TODO(), streamOpts)
if err != nil { if err == nil {
errChan <- err
return return
} }
log.Warn().
Err(err).
Str("context", "StartExecProcess").
Msg("WebSocket exec failed, falling back to SPDY")
} }
err = exec.StreamWithContext(context.TODO(), remotecommand.StreamOptions{ // Fall back to SPDY executor
Stdin: stdin, exec, err = remotecommand.NewSPDYExecutor(config, "POST", req.URL())
Stdout: stdout, if err != nil {
Tty: true, errChan <- fmt.Errorf("unable to create SPDY executor: %w", err)
}) return
}
err = exec.StreamWithContext(context.TODO(), streamOpts)
if err != nil { if err != nil {
var exitError utilexec.ExitError var exitError utilexec.ExitError
if !errors.As(err, &exitError) { if !errors.As(err, &exitError) {
errChan <- errors.New("unable to start exec process") errChan <- fmt.Errorf("unable to start exec process: %w", err)
} }
} }
} }
-1
View File
@@ -74,7 +74,6 @@ func Test_GenerateYAML(t *testing.T) {
name: portainer-ctx name: portainer-ctx
current-context: portainer-ctx current-context: portainer-ctx
kind: Config kind: Config
preferences: {}
users: users:
- name: test-user - name: test-user
user: user:
+2 -2
View File
@@ -576,7 +576,7 @@ type (
} }
PolicyChartBundle struct { PolicyChartBundle struct {
PolicyChartSummary PolicyChartSummary `mapstructure:",squash"`
EncodedTgz string `json:"EncodedTgz"` EncodedTgz string `json:"EncodedTgz"`
Namespace string `json:"Namespace"` Namespace string `json:"Namespace"`
PreReleaseManifest string `json:"PreReleaseManifest,omitempty"` PreReleaseManifest string `json:"PreReleaseManifest,omitempty"`
@@ -1874,7 +1874,7 @@ type (
const ( const (
// APIVersion is the version number of the Portainer API // APIVersion is the version number of the Portainer API
APIVersion = "2.39.0" APIVersion = "2.39.1"
// Support annotation for the API version ("STS" for Short-Term Support or "LTS" for Long-Term Support) // Support annotation for the API version ("STS" for Short-Term Support or "LTS" for Long-Term Support)
APIVersionSupport = "LTS" APIVersionSupport = "LTS"
// Edition is what this edition of Portainer is called // Edition is what this edition of Portainer is called
+2 -10
View File
@@ -74,18 +74,10 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
} }
} }
if err := d.composeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{ return d.composeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{
ComposeOptions: options, ComposeOptions: options,
ForceRecreate: forceRecreate, ForceRecreate: forceRecreate,
}); err != nil { })
if err := d.composeStackManager.Down(context.TODO(), stack, endpoint); err != nil {
log.Warn().Err(err).Msg("failed to cleanup compose stack after failed deployment")
}
return err
}
return nil
} }
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error { func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
+34
View File
@@ -0,0 +1,34 @@
package stackutils
import (
"maps"
"os"
"path"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/compose-spec/compose-go/v2/dotenv"
)
// BuildEnvMap builds the environment variable map for stack validation/loading.
// Priority (lowest to highest): OS env → .env file → stack.Env
func BuildEnvMap(stack *portainer.Stack) map[string]string {
env := make(map[string]string, len(os.Environ()))
for _, e := range os.Environ() {
k, v, _ := strings.Cut(e, "=")
env[k] = v
}
dotEnvPath := filesystem.JoinPaths(stack.ProjectPath, path.Dir(stack.EntryPoint), ".env")
if dotVars, err := dotenv.Read(dotEnvPath); err == nil {
maps.Copy(env, dotVars)
}
for _, pair := range stack.Env {
env[pair.Name] = pair.Value
}
return env
}
+38 -30
View File
@@ -1,38 +1,38 @@
package stackutils package stackutils
import ( import (
portainer "github.com/portainer/portainer/api" "context"
"path"
"github.com/docker/cli/cli/compose/loader" portainer "github.com/portainer/portainer/api"
"github.com/docker/cli/cli/compose/types" "github.com/portainer/portainer/api/filesystem"
composeloader "github.com/compose-spec/compose-go/v2/loader"
composetypes "github.com/compose-spec/compose-go/v2/types"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error { type StackFileValidationConfig struct {
composeConfigYAML, err := loader.ParseYAML(stackFileContent) Content []byte
SecuritySettings *portainer.EndpointSecuritySettings
Env map[string]string
WorkingDir string
}
func IsValidStackFile(config StackFileValidationConfig) error {
composeConfigDetails := composetypes.ConfigDetails{
ConfigFiles: []composetypes.ConfigFile{{Content: config.Content}},
Environment: config.Env,
WorkingDir: config.WorkingDir,
}
composeConfig, err := composeloader.LoadWithContext(context.Background(), composeConfigDetails, composeloader.WithSkipValidation)
if err != nil { if err != nil {
return err return err
} }
composeConfigFile := types.ConfigFile{ for _, service := range composeConfig.Services {
Config: composeConfigYAML, if !config.SecuritySettings.AllowBindMountsForRegularUsers {
}
composeConfigDetails := types.ConfigDetails{
ConfigFiles: []types.ConfigFile{composeConfigFile},
Environment: map[string]string{},
}
composeConfig, err := loader.Load(composeConfigDetails, func(options *loader.Options) {
options.SkipValidation = true
})
if err != nil {
return err
}
for key := range composeConfig.Services {
service := composeConfig.Services[key]
if !securitySettings.AllowBindMountsForRegularUsers {
for _, volume := range service.Volumes { for _, volume := range service.Volumes {
if volume.Type == "bind" { if volume.Type == "bind" {
return errors.New("bind-mount disabled for non administrator users") return errors.New("bind-mount disabled for non administrator users")
@@ -40,23 +40,23 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo
} }
} }
if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged { if !config.SecuritySettings.AllowPrivilegedModeForRegularUsers && service.Privileged {
return errors.New("privileged mode disabled for non administrator users") return errors.New("privileged mode disabled for non administrator users")
} }
if !securitySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" { if !config.SecuritySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
return errors.New("pid host disabled for non administrator users") return errors.New("pid host disabled for non administrator users")
} }
if !securitySettings.AllowDeviceMappingForRegularUsers && len(service.Devices) > 0 { if !config.SecuritySettings.AllowDeviceMappingForRegularUsers && len(service.Devices) > 0 {
return errors.New("device mapping disabled for non administrator users") return errors.New("device mapping disabled for non administrator users")
} }
if !securitySettings.AllowSysctlSettingForRegularUsers && len(service.Sysctls) > 0 { if !config.SecuritySettings.AllowSysctlSettingForRegularUsers && len(service.Sysctls) > 0 {
return errors.New("sysctl setting disabled for non administrator users") return errors.New("sysctl setting disabled for non administrator users")
} }
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) { if !config.SecuritySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
return errors.New("container capabilities disabled for non administrator users") return errors.New("container capabilities disabled for non administrator users")
} }
} }
@@ -65,13 +65,21 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo
} }
func ValidateStackFiles(stack *portainer.Stack, securitySettings *portainer.EndpointSecuritySettings, fileService portainer.FileService) error { func ValidateStackFiles(stack *portainer.Stack, securitySettings *portainer.EndpointSecuritySettings, fileService portainer.FileService) error {
env := BuildEnvMap(stack)
workingDir := filesystem.JoinPaths(stack.ProjectPath, path.Dir(stack.EntryPoint))
for _, file := range GetStackFilePaths(stack, false) { for _, file := range GetStackFilePaths(stack, false) {
stackContent, err := fileService.GetFileContent(stack.ProjectPath, file) stackContent, err := fileService.GetFileContent(stack.ProjectPath, file)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to get stack file content") return errors.Wrap(err, "failed to get stack file content")
} }
if err := IsValidStackFile(stackContent, securitySettings); err != nil { if err := IsValidStackFile(StackFileValidationConfig{
Content: stackContent,
SecuritySettings: securitySettings,
Env: env,
WorkingDir: workingDir,
}); err != nil {
return errors.Wrap(err, "stack config file is invalid") return errors.Wrap(err, "stack config file is invalid")
} }
} }
+225 -20
View File
@@ -1,10 +1,11 @@
package stackutils package stackutils
import ( import (
"os"
"path/filepath"
"testing" "testing"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -30,32 +31,236 @@ networks:
`) `)
securitySettings := &portainer.EndpointSecuritySettings{} securitySettings := &portainer.EndpointSecuritySettings{}
err := IsValidStackFile(yamlContent, securitySettings) err := IsValidStackFile(StackFileValidationConfig{
Content: yamlContent,
SecuritySettings: securitySettings,
})
require.NoError(t, err) require.NoError(t, err)
} }
func TestIsValidStackFile_PortEnv(t *testing.T) { // TestIsValidStackFile_MissingEnvVarBehavior documents how port variable position affects
yamlContent := []byte(` // validation when the env var is not provided. Docker accepts an empty host port (left side)
// but requires a valid container port (right side).
func TestIsValidStackFile_MissingEnvVarBehavior(t *testing.T) {
securitySettings := &portainer.EndpointSecuritySettings{}
t.Run("var on left side only passes (docker allows :9090)", func(t *testing.T) {
err := IsValidStackFile(StackFileValidationConfig{
Content: []byte(`
version: "3"
services:
api:
image: nginx
ports:
- "${API_PORT}:9090"
`),
SecuritySettings: securitySettings,
})
require.NoError(t, err)
})
t.Run("var on right side fails", func(t *testing.T) {
err := IsValidStackFile(StackFileValidationConfig{
Content: []byte(`
version: "3"
services:
api:
image: nginx
ports:
- "9090:${API_PORT}"
`),
SecuritySettings: securitySettings,
})
require.Error(t, err)
})
t.Run("var on both sides fails", func(t *testing.T) {
err := IsValidStackFile(StackFileValidationConfig{
Content: []byte(`
version: "3"
services:
api:
image: nginx
ports:
- "${API_PORT}:${API_PORT}"
`),
SecuritySettings: securitySettings,
})
require.Error(t, err)
})
}
func TestIsValidStackFile_EnvVarInBothPortFields(t *testing.T) {
securitySettings := &portainer.EndpointSecuritySettings{}
err := IsValidStackFile(StackFileValidationConfig{
Content: []byte(`
version: "3" version: "3"
services: services:
webservice: api:
image: nginx image: nginx
container_name: hello-world
networks:
- "mynet1"
ports: ports:
- "${PORT}:80" - "${API_PORT}:${API_PORT}"
`),
networks: SecuritySettings: securitySettings,
mynet1: Env: map[string]string{"API_PORT": "3000"},
driver: bridge })
ipam:
config:
- subnet: 172.16.0.0/24
`)
securitySettings := &portainer.EndpointSecuritySettings{}
err := IsValidStackFile(yamlContent, securitySettings)
require.NoError(t, err) require.NoError(t, err)
} }
type mockFileService struct {
portainer.FileService
fileContent []byte
projectVersionPath string
}
func (m mockFileService) GetFileContent(trustedRootPath, filePath string) ([]byte, error) {
return m.fileContent, nil
}
func (m mockFileService) FormProjectPathByVersion(projectPath string, version int, commitHash string) string {
return m.projectVersionPath
}
func TestValidateStackFiles_EnvVars(t *testing.T) {
fileContent := []byte(`
version: "3"
services:
api:
image: nginx
ports:
- "${API_PORT}:${API_PORT}"
`)
stack := &portainer.Stack{
ProjectPath: "/tmp/stack/1",
EntryPoint: "docker-compose.yml",
Env: []portainer.Pair{{Name: "API_PORT", Value: "3000"}},
}
fileService := mockFileService{
fileContent: fileContent,
projectVersionPath: "/tmp/stack/1",
}
securitySettings := &portainer.EndpointSecuritySettings{}
err := ValidateStackFiles(stack, securitySettings, fileService)
require.NoError(t, err)
}
func TestValidateStackFiles_OSEnvVar(t *testing.T) {
t.Setenv("HOST_PORT", "3000")
fileContent := []byte(`
version: "3"
services:
api:
image: nginx
ports:
- "80:${HOST_PORT}"
`)
stack := &portainer.Stack{
ProjectPath: "/tmp/stack/1",
EntryPoint: "docker-compose.yml",
}
fileService := mockFileService{
fileContent: fileContent,
projectVersionPath: "/tmp/stack/1",
}
securitySettings := &portainer.EndpointSecuritySettings{}
err := ValidateStackFiles(stack, securitySettings, fileService)
require.NoError(t, err)
}
func TestValidateStackFiles_DotEnvFile(t *testing.T) {
tmpDir := t.TempDir()
err := os.WriteFile(filepath.Join(tmpDir, ".env"), []byte("HOST_PORT=3000\n"), 0600)
require.NoError(t, err)
fileContent := []byte(`
version: "3"
services:
api:
image: nginx
ports:
- "80:${HOST_PORT}"
`)
stack := &portainer.Stack{
ProjectPath: tmpDir,
EntryPoint: "docker-compose.yml",
}
fileService := mockFileService{
fileContent: fileContent,
projectVersionPath: tmpDir,
}
securitySettings := &portainer.EndpointSecuritySettings{}
err = ValidateStackFiles(stack, securitySettings, fileService)
require.NoError(t, err)
}
func TestValidateStackFiles_EnvFileAttribute(t *testing.T) {
tmpDir := t.TempDir()
err := os.WriteFile(filepath.Join(tmpDir, "web.env"), []byte("HOST_PORT=3000\n"), 0600)
require.NoError(t, err)
fileContent := []byte(`
version: "3"
services:
api:
image: nginx
env_file:
- ./web.env
`)
stack := &portainer.Stack{
ProjectPath: tmpDir,
EntryPoint: "docker-compose.yml",
}
fileService := mockFileService{
fileContent: fileContent,
projectVersionPath: tmpDir,
}
securitySettings := &portainer.EndpointSecuritySettings{}
err = ValidateStackFiles(stack, securitySettings, fileService)
require.NoError(t, err)
}
func TestValidateStackFiles_BindMountBlockedForNonAdmin(t *testing.T) {
fileContent := []byte(`
version: "3"
services:
api:
image: nginx
volumes:
- /host/path:/container/path
`)
stack := &portainer.Stack{
ProjectPath: "/tmp/stack/1",
EntryPoint: "docker-compose.yml",
}
fileService := mockFileService{
fileContent: fileContent,
projectVersionPath: "/tmp/stack/1",
}
securitySettings := &portainer.EndpointSecuritySettings{
AllowBindMountsForRegularUsers: false,
}
err := ValidateStackFiles(stack, securitySettings, fileService)
require.ErrorContains(t, err, "bind-mount disabled for non administrator users")
}
+21
View File
@@ -25,3 +25,24 @@ func StackResourceControlGetter[
func StackResourceControlID(endpointID portainer.EndpointID, name string) string { func StackResourceControlID(endpointID portainer.EndpointID, name string) string {
return stackutils.ResourceControlID(endpointID, name) return stackutils.ResourceControlID(endpointID, name)
} }
type ExternalStack struct {
Labels map[string]string
}
// External stacks are indirectly detected either via containers or services labels
// Any UAC applied to them can only be fetched from containers'/services' labels
func ExternalStackResourceControlGetter[
TX txLike[RCS, TS, US],
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
](
tx TX,
endpointID portainer.EndpointID) func(item ExternalStack) (*portainer.ResourceControl, error) {
return genericResourcControlGetter(tx, endpointID, ResourceContext[ExternalStack]{
RCType: portainer.StackResourceControl,
IDGetter: func(s ExternalStack) string { return "0" },
LabelsGetter: func(es ExternalStack) map[string]string { return es.Labels },
})
}
@@ -66,7 +66,6 @@
.pagination > .active > span:focus, .pagination > .active > span:focus,
.pagination > .active > button:focus { .pagination > .active > button:focus {
@apply text-blue-7; @apply text-blue-7;
z-index: 3;
cursor: default; cursor: default;
/* background-color: var(--text-pagination-span-color); */ /* background-color: var(--text-pagination-span-color); */
background-color: var(--bg-pagination-color); background-color: var(--bg-pagination-color);
+2 -2
View File
@@ -65,7 +65,7 @@ const SheetOverlay = forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
className={clsx( className={clsx(
'fixed inset-0 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className className
)} )}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
@@ -76,7 +76,7 @@ const SheetOverlay = forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva( const sheetVariants = cva(
'fixed gap-4 bg-widget-color p-5 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', 'fixed z-50 bg-widget-color p-5 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{ {
variants: { variants: {
side: { side: {
@@ -15,11 +15,10 @@ export function StickyFooter({
<div <div
className={clsx( className={clsx(
styles.actionBar, styles.actionBar,
// The sticky footer should be below the modal overlay `Modal.tsx` and react select menu `ReactSelect.css` (z-50) 'fixed bottom-0 right-0 z-40 h-16',
'fixed bottom-0 right-0 z-10 h-16',
'flex items-center px-6', 'flex items-center px-6',
'bg-[var(--bg-widget-color)] border-t border-[var(--border-widget-color)]', 'bg-[var(--bg-widget-color)] border-t border-[var(--border-widget-color)]',
'shadow-[0_-2px_10px_rgba(0,0,0,0.1)]', 'shadow-[0_-2px_5px_rgba(0,0,0,0.1)]',
className className
)} )}
> >
+12 -3
View File
@@ -1,12 +1,13 @@
import { Trash2 } from 'lucide-react'; import { Trash2 } from 'lucide-react';
import { ComponentProps, PropsWithChildren, ReactNode } from 'react'; import { ComponentProps, PropsWithChildren, ReactNode } from 'react';
import clsx from 'clsx';
import { AutomationTestingProps } from '@/types'; import { AutomationTestingProps } from '@/types';
import { confirmDelete } from '@@/modals/confirm'; import { confirmDelete } from '@@/modals/confirm';
import { Button } from './Button';
import { LoadingButton } from './LoadingButton'; import { LoadingButton } from './LoadingButton';
import { Button } from './Button';
type ConfirmOrClick = type ConfirmOrClick =
| { | {
@@ -26,7 +27,10 @@ export function DeleteButton({
size, size,
children, children,
isLoading, isLoading,
text = 'Remove',
loadingText = 'Removing...', loadingText = 'Removing...',
icon = false,
type,
'data-cy': dataCy, 'data-cy': dataCy,
...props ...props
}: PropsWithChildren< }: PropsWithChildren<
@@ -35,7 +39,10 @@ export function DeleteButton({
size?: ComponentProps<typeof Button>['size']; size?: ComponentProps<typeof Button>['size'];
disabled?: boolean; disabled?: boolean;
isLoading?: boolean; isLoading?: boolean;
text?: string;
loadingText?: string; loadingText?: string;
icon?: boolean;
type?: ComponentProps<typeof Button>['type'];
} }
>) { >) {
if (isLoading === undefined) { if (isLoading === undefined) {
@@ -46,10 +53,11 @@ export function DeleteButton({
disabled={disabled || isLoading} disabled={disabled || isLoading}
onClick={() => handleClick()} onClick={() => handleClick()}
icon={Trash2} icon={Trash2}
className="!m-0" className={clsx('!m-0', icon ? 'btn-icon' : '')}
data-cy={dataCy} data-cy={dataCy}
type={type}
> >
{children || 'Remove'} {children || text}
</Button> </Button>
); );
} }
@@ -65,6 +73,7 @@ export function DeleteButton({
data-cy={dataCy} data-cy={dataCy}
isLoading={isLoading} isLoading={isLoading}
loadingText={loadingText} loadingText={loadingText}
type={type}
> >
{children || 'Remove'} {children || 'Remove'}
</LoadingButton> </LoadingButton>
@@ -1,3 +1,5 @@
import { useMemo } from 'react';
import { difference } from 'lodash';
import { import {
getCoreRowModel, getCoreRowModel,
getFilteredRowModel, getFilteredRowModel,
@@ -14,6 +16,7 @@ import { defaultGetRowId } from './defaultGetRowId';
import { Table } from './Table'; import { Table } from './Table';
import { NestedTable } from './NestedTable'; import { NestedTable } from './NestedTable';
import { DatatableContent } from './DatatableContent'; import { DatatableContent } from './DatatableContent';
import { DatatableFooter } from './DatatableFooter';
import { BasicTableSettings, DefaultType } from './types'; import { BasicTableSettings, DefaultType } from './types';
interface Props<D extends DefaultType> extends AutomationTestingProps { interface Props<D extends DefaultType> extends AutomationTestingProps {
@@ -69,6 +72,18 @@ export function NestedDatatable<D extends DefaultType>({
...(enablePagination && { getPaginationRowModel: getPaginationRowModel() }), ...(enablePagination && { getPaginationRowModel: getPaginationRowModel() }),
}); });
const tableState = tableInstance.getState();
const selectedRowModel = tableInstance.getSelectedRowModel();
const selectedItems = selectedRowModel.rows.map((row) => row.original);
const filteredItems = tableInstance
.getFilteredRowModel()
.rows.map((row) => row.original);
const hiddenSelectedItems = useMemo(
() => difference(selectedItems, filteredItems),
[selectedItems, filteredItems]
);
return ( return (
<NestedTable> <NestedTable>
<Table.Container noWidget> <Table.Container noWidget>
@@ -80,6 +95,17 @@ export function NestedDatatable<D extends DefaultType>({
aria-label={ariaLabel} aria-label={ariaLabel}
data-cy={dataCy} data-cy={dataCy}
/> />
{enablePagination && (
<DatatableFooter
onPageChange={tableInstance.setPageIndex}
onPageSizeChange={tableInstance.setPageSize}
page={tableState.pagination.pageIndex}
pageSize={tableState.pagination.pageSize}
pageCount={tableInstance.getPageCount()}
totalSelected={selectedItems.length}
totalHiddenSelected={hiddenSelectedItems.length}
/>
)}
</Table.Container> </Table.Container>
</NestedTable> </NestedTable>
); );
+21 -9
View File
@@ -7,31 +7,43 @@ interface Props<D extends DefaultType = DefaultType> {
cells: Cell<D, unknown>[]; cells: Cell<D, unknown>[];
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
'aria-selected'?: boolean;
} }
export function TableRow<D extends DefaultType = DefaultType>({ export function TableRow<D extends DefaultType = DefaultType>({
cells, cells,
className, className,
onClick, onClick,
'aria-selected': ariaSelected,
}: Props<D>) { }: Props<D>) {
return ( return (
<tr <tr
className={clsx(className, { 'cursor-pointer': !!onClick })} className={clsx(className, { 'cursor-pointer': !!onClick })}
onClick={onClick} onClick={onClick}
aria-selected={ariaSelected}
> >
{cells.map((cell) => ( {cells.map((cell) => {
<td key={cell.id} className={getClassName(cell.column.columnDef.meta)}> const { className, width } = parseMeta(cell.column.columnDef.meta);
{flexRender(cell.column.columnDef.cell, cell.getContext())} return (
</td> <td key={cell.id} className={className} style={{ width }}>
))} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
})}
</tr> </tr>
); );
} }
function getClassName<D extends DefaultType = DefaultType>( function parseMeta<D extends DefaultType = DefaultType>(
meta: ColumnMeta<D, unknown> | undefined meta: ColumnMeta<D, unknown> | undefined
) { ) {
return !!meta && 'className' in meta && typeof meta.className === 'string' const className =
? meta.className !!meta && 'className' in meta && typeof meta.className === 'string'
: ''; ? meta.className
: '';
const width =
!!meta && 'width' in meta && typeof meta.width === 'string'
? meta.width
: undefined;
return { className, width };
} }
@@ -67,7 +67,7 @@ export function createSelectColumn<T>(dataCy: string): ColumnDef<T> {
), ),
enableHiding: false, enableHiding: false,
meta: { meta: {
width: 50, width: '50px',
}, },
}; };
} }
@@ -52,7 +52,7 @@ export function FormSection({
</FormSectionTitle> </FormSectionTitle>
{/* col-sm-12 in the title has a 'float: left' style - 'clear-both' makes sure it doesn't get in the way of the next div */} {/* col-sm-12 in the title has a 'float: left' style - 'clear-both' makes sure it doesn't get in the way of the next div */}
{/* https://stackoverflow.com/questions/7759837/put-divs-below-floatleft-divs */} {/* https://stackoverflow.com/questions/7759837/put-divs-below-floatleft-divs */}
{isExpanded && <div className="clear-both">{children}</div>} <div className="clear-both">{isExpanded && children}</div>
</div> </div>
); );
} }
+4 -4
View File
@@ -37,11 +37,11 @@ export function Modal({
<Context.Provider value> <Context.Provider value>
<DialogOverlay <DialogOverlay
isOpen isOpen
className={clsx( className={clsx(styles.overlay, 'flex items-center justify-center')}
styles.overlay,
'flex items-center justify-center z-50'
)}
onDismiss={onDismiss} onDismiss={onDismiss}
// When a Sheet is open and then a Modal opens, Radix DismissableLayer sets body.style.pointerEvents="none" for this modal overlay, so make it auto here.
// z-index ensures the modal renders above the base views and any Sheet (z-50).
style={{ zIndex: 60, pointerEvents: 'auto' }}
> >
<DialogContent <DialogContent
aria-label={ariaLabel} aria-label={ariaLabel}
@@ -5,7 +5,7 @@ import { ContainerDetailsViewModel } from '@/docker/models/containerDetails';
import { ResourceControlType } from '@/react/portainer/access-control/types'; import { ResourceControlType } from '@/react/portainer/access-control/types';
import { trimContainerName } from '@/docker/filters/utils'; import { trimContainerName } from '@/docker/filters/utils';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useRegistries } from '@/react/portainer/registries/queries/useRegistries'; import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
import { Registry } from '@/react/portainer/registries/types/registry'; import { Registry } from '@/react/portainer/registries/types/registry';
import { PageHeader } from '@@/PageHeader'; import { PageHeader } from '@@/PageHeader';
@@ -32,7 +32,7 @@ export function ItemView() {
{ select: (c) => new ContainerDetailsViewModel(c) } { select: (c) => new ContainerDetailsViewModel(c) }
); );
const registriesQuery = useRegistries(); const registriesQuery = useEnvironmentRegistries(environmentId);
if ( if (
containerQuery.isLoading || containerQuery.isLoading ||
@@ -25,6 +25,7 @@ export function TasksDatatable({
search={search} search={search}
aria-label="Tasks table" aria-label="Tasks table"
data-cy="docker-service-tasks-nested-datatable" data-cy="docker-service-tasks-nested-datatable"
initialSortBy={{ id: 'Updated', desc: true }}
/> />
); );
} }
@@ -117,6 +117,7 @@ export function StackEditorTab({
envType={envType} envType={envType}
schema={schemaQuery.data} schema={schemaQuery.data}
versions={versions} versions={versions}
isSubmitting={mutation.isLoading}
isSaved={mutation.isSuccess} isSaved={mutation.isSuccess}
webhookId={webhookId} webhookId={webhookId}
/> />
@@ -43,6 +43,7 @@ const defaultProps = {
schema: { type: 'object' } as JSONSchema7, schema: { type: 'object' } as JSONSchema7,
isOrphaned: false, isOrphaned: false,
stackId: 1, stackId: 1,
isSubmitting: false,
isSaved: false, isSaved: false,
webhookId: '', webhookId: '',
}; };
@@ -377,17 +378,7 @@ describe('form submission', () => {
}); });
it('should show loading text during submission', async () => { it('should show loading text during submission', async () => {
const onSubmit = vi.fn().mockImplementation(() => new Promise(() => {})); // Never resolves renderComponent({ isSubmitting: true }, {});
renderComponent({}, { onSubmit });
const user = userEvent.setup();
await waitFor(() => {
const deployButton = screen.getByTestId('stack-deploy-button');
expect(deployButton).toBeEnabled();
});
const deployButton = screen.getByTestId('stack-deploy-button');
await user.click(deployButton);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(/Deployment in progress.../)).toBeInTheDocument(); expect(screen.getByText(/Deployment in progress.../)).toBeInTheDocument();
@@ -29,6 +29,7 @@ interface StackEditorTabInnerProps {
versions?: Array<number>; versions?: Array<number>;
stackId: Stack['Id']; stackId: Stack['Id'];
isSaved: boolean; isSaved: boolean;
isSubmitting: boolean;
webhookId: string; webhookId: string;
} }
@@ -42,20 +43,15 @@ export function StackEditorTabInner({
versions, versions,
stackId, stackId,
isSaved, isSaved,
isSubmitting,
webhookId, webhookId,
}: StackEditorTabInnerProps) { }: StackEditorTabInnerProps) {
const { authorized: isAuthorizedToUpdate } = useAuthorizations( const { authorized: isAuthorizedToUpdate } = useAuthorizations(
'PortainerStackUpdate' 'PortainerStackUpdate'
); );
const { const { values, errors, setFieldValue, isValid, initialValues } =
values, useFormikContext<StackEditorFormValues>();
errors,
setFieldValue,
isSubmitting,
isValid,
initialValues,
} = useFormikContext<StackEditorFormValues>();
usePreventExit( usePreventExit(
initialValues.stackFileContent, initialValues.stackFileContent,
@@ -173,24 +173,19 @@ describe('getEnvironmentOptions', () => {
}); });
}); });
it('should create "Unassigned" group for GroupId = 1', () => { it('should auto create an Others group if group is missing', () => {
const environments: Environment[] = [ const environments: Environment[] = [
{ Id: 1, Name: 'Env 1', GroupId: 1 } as Environment, { Id: 1, Name: 'Env 1', GroupId: 1 } as Environment,
{ Id: 2, Name: 'Env 2', GroupId: 2 } as Environment,
]; ];
const groups: EnvironmentGroup[] = [];
const result = getEnvironmentOptions([], environments); const result = getEnvironmentOptions(groups, environments);
expect(result.length).toBe(1);
expect(result[0].label).toBe('Unassigned'); expect(result[0].label).toBe('Others');
expect(result[0].options[0]).toEqual({ label: 'Env 1', value: 1 }); expect(result[0].options).toEqual([
}); { label: 'Env 1', value: 1 },
{ label: 'Env 2', value: 2 },
it('should throw error if group is missing for non-unassigned GroupId', () => { ]);
const environments: Environment[] = [
{ Id: 1, Name: 'Env 1', GroupId: 2 } as Environment,
];
expect(() => getEnvironmentOptions([], environments)).toThrow(
'Missing group with id 2'
);
}); });
}); });
@@ -1,4 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { sortBy } from 'lodash';
import { useEnvironmentList } from '@/react/portainer/environments/queries'; import { useEnvironmentList } from '@/react/portainer/environments/queries';
import { useGroups } from '@/react/portainer/environments/environment-groups/queries'; import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
@@ -73,7 +74,11 @@ export function getEnvironmentOptions(
return acc; return acc;
} }
const groupId = environment.GroupId; let groupId = environment.GroupId;
if (!groups.some((g) => g.Id === groupId)) {
groupId = -1;
}
if (!acc[groupId]) { if (!acc[groupId]) {
acc[groupId] = []; acc[groupId] = [];
} }
@@ -87,13 +92,10 @@ export function getEnvironmentOptions(
return Object.entries(groupedEnvironments).map(([groupId, envOptions]) => { return Object.entries(groupedEnvironments).map(([groupId, envOptions]) => {
const parsedGroupId = parseInt(groupId, 10); const parsedGroupId = parseInt(groupId, 10);
const group = groups.find((g) => g.Id === parsedGroupId); const group = groups.find((g) => g.Id === parsedGroupId);
if (!group && parsedGroupId !== 1) {
throw new Error(`Missing group with id ${groupId}`);
}
return { return {
label: group?.Name || 'Unassigned', label: group?.Name || 'Others',
options: envOptions, options: sortBy(envOptions, 'label'),
}; };
}); });
} }
@@ -1,14 +1,11 @@
import { Pencil } from 'lucide-react';
import { useCurrentStateAndParams } from '@uirouter/react'; import { useCurrentStateAndParams } from '@uirouter/react';
import { Pod } from 'kubernetes-types/core/v1'; import { Pod } from 'kubernetes-types/core/v1';
import { Authorized, useIsEdgeAdmin } from '@/react/hooks/useUser'; import { Authorized } from '@/react/hooks/useUser';
import { useNamespaceQuery } from '@/react/kubernetes/namespaces/queries/useNamespaceQuery'; import { useNamespaceQuery } from '@/react/kubernetes/namespaces/queries/useNamespaceQuery';
import { Widget, WidgetBody } from '@@/Widget'; import { Widget, WidgetBody } from '@@/Widget';
import { AddButton, Button } from '@@/buttons'; import { AddButton } from '@@/buttons';
import { Link } from '@@/Link';
import { Icon } from '@@/Icon';
import { applicationIsKind, isExternalApplication } from '../../utils'; import { applicationIsKind, isExternalApplication } from '../../utils';
import { appStackIdLabel, appStackKindLabel } from '../../constants'; import { appStackIdLabel, appStackKindLabel } from '../../constants';
@@ -17,6 +14,8 @@ import { useApplicationServices } from '../../queries/useApplicationServices';
import { useAppStackFile } from '../../queries/useAppStackFile'; import { useAppStackFile } from '../../queries/useAppStackFile';
import { Application } from '../../types'; import { Application } from '../../types';
import { EdgeEditButton } from './EdgeEditButton';
import { EditButton } from './EditButton';
import { RestartApplicationButton } from './RestartApplicationButton'; import { RestartApplicationButton } from './RestartApplicationButton';
import { RedeployApplicationButton } from './RedeployApplicationButton'; import { RedeployApplicationButton } from './RedeployApplicationButton';
import { RollbackApplicationButton } from './RollbackApplicationButton'; import { RollbackApplicationButton } from './RollbackApplicationButton';
@@ -42,9 +41,6 @@ export function ApplicationDetailsWidget() {
const namespaceData = useNamespaceQuery(environmentId, namespace); const namespaceData = useNamespaceQuery(environmentId, namespace);
const isSystemNamespace = namespaceData.data?.IsSystem; const isSystemNamespace = namespaceData.data?.IsSystem;
// check if user is edge admin
const edgeAdminQuery = useIsEdgeAdmin();
// get app info // get app info
const { data: app } = useApplication( const { data: app } = useApplication(
environmentId, environmentId,
@@ -81,35 +77,15 @@ export function ApplicationDetailsWidget() {
{!isSystemNamespace && ( {!isSystemNamespace && (
<div className="mb-4 flex flex-wrap gap-2"> <div className="mb-4 flex flex-wrap gap-2">
<Authorized authorizations="K8sApplicationDetailsW"> <Authorized authorizations="K8sApplicationDetailsW">
<Link {appStackKind === 'edge' ? (
to={ <EdgeEditButton stackId={appStackId} />
appStackKind === 'edge' ) : (
? 'edge.stacks.edit' <EditButton to=".edit">
: 'kubernetes.applications.application.edit'
}
params={
appStackKind === 'edge'
? { stackId: appStackId }
: undefined
}
data-cy="k8sAppDetail-editAppLink"
>
<Button
type="button"
color="light"
size="small"
className="hover:decoration-none !ml-0"
data-cy="k8sAppDetail-editAppButton"
disabled={
edgeAdminQuery.isLoading || !edgeAdminQuery.isAdmin
}
>
<Icon icon={Pencil} className="mr-1" />
{externalApp {externalApp
? 'Edit external application' ? 'Edit external application'
: 'Edit this application'} : 'Edit this application'}
</Button> </EditButton>
</Link> )}
</Authorized> </Authorized>
{!applicationIsKind<Pod>('Pod', app) && ( {!applicationIsKind<Pod>('Pod', app) && (
<> <>
@@ -0,0 +1,83 @@
import { render, screen } from '@testing-library/react';
import { vi } from 'vitest';
import { EdgeEditButton } from './EdgeEditButton';
const mockUseIsEdgeAdmin = vi.fn();
vi.mock('@/react/hooks/useUser', () => ({
useIsEdgeAdmin: () => mockUseIsEdgeAdmin(),
}));
vi.mock('@@/Tip/TooltipWithChildren', () => ({
TooltipWithChildren: ({
children,
message,
}: {
children: React.ReactNode;
message: string;
}) => (
<div data-cy="tooltip" data-message={message}>
{children}
</div>
),
}));
vi.mock('@@/buttons', () => ({
Button: ({
children,
disabled,
'data-cy': dataCy,
}: {
children: React.ReactNode;
disabled?: boolean;
'data-cy'?: string;
}) => (
<button disabled={disabled} data-cy={dataCy} type="button">
{children}
</button>
),
}));
vi.mock('@@/Link', () => ({
Link: ({ children }: { children: React.ReactNode }) => (
<a href="/">{children}</a>
),
}));
describe('EdgeEditButton', () => {
it('renders disabled button without tooltip while loading', () => {
mockUseIsEdgeAdmin.mockReturnValue({ isLoading: true, isAdmin: false });
render(<EdgeEditButton stackId={1} />);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument();
});
it('renders enabled button without tooltip for edge admin', () => {
mockUseIsEdgeAdmin.mockReturnValue({ isLoading: false, isAdmin: true });
render(<EdgeEditButton stackId={1} />);
const button = screen.getByRole('button');
expect(button).not.toBeDisabled();
expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument();
});
it('renders disabled button with tooltip for non-admin', () => {
mockUseIsEdgeAdmin.mockReturnValue({ isLoading: false, isAdmin: false });
render(<EdgeEditButton stackId={1} />);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
const tooltip = screen.getByTestId('tooltip');
expect(tooltip).toHaveAttribute(
'data-message',
'This application is managed by an edge stack and can only be edited by an edge administrator'
);
});
});
@@ -0,0 +1,35 @@
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
import { EditButton } from './EditButton';
interface Props {
stackId?: number;
}
export function EdgeEditButton({ stackId }: Props) {
const edgeAdminQuery = useIsEdgeAdmin();
const isDisabled = edgeAdminQuery.isLoading || !edgeAdminQuery.isAdmin;
const button = (
<EditButton
to="edge.stacks.edit"
params={{ stackId }}
disabled={isDisabled}
>
Manage edge stack
</EditButton>
);
if (edgeAdminQuery.isLoading || edgeAdminQuery.isAdmin) {
return button;
}
return (
<TooltipWithChildren message="This application is managed by an edge stack and can only be edited by an edge administrator">
<span>{button}</span>
</TooltipWithChildren>
);
}
@@ -0,0 +1,34 @@
import { PencilIcon } from 'lucide-react';
import { PropsWithChildren } from 'react';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
interface Props {
to?: string;
params?: Record<string, unknown>;
disabled?: boolean;
}
export function EditButton({
to = '',
params,
children,
disabled,
}: PropsWithChildren<Props>) {
return (
<Button
type="button"
color="light"
size="small"
data-cy="k8sAppDetail-editAppButton"
disabled={disabled}
as={disabled ? 'button' : Link}
props={disabled ? undefined : { to, params }}
icon={PencilIcon}
>
{children}
</Button>
);
}
@@ -30,7 +30,7 @@ export function CreateGroupView() {
]} ]}
/> />
<div className="row"> <div className="row pb-20">
<div className="col-sm-12"> <div className="col-sm-12">
<Widget> <Widget>
<Widget.Body> <Widget.Body>
@@ -298,14 +298,13 @@ describe('EditGroupView', () => {
await user.click(submitButton); await user.click(submitButton);
// Verify the request URL and body // Verify the request URL and body.
await waitFor(() => { await waitFor(() => {
expect(requestUrl).toBe('/api/endpoint_groups/2'); expect(requestUrl).toBe('/api/endpoint_groups/2');
expect(requestBody).toEqual({ expect(requestBody).toEqual({
Name: 'Updated Group', Name: 'Updated Group',
Description: 'Test description', Description: 'Test description',
TagIDs: [1], TagIDs: [1],
AssociatedEndpoints: [1], // The associated environment ID
}); });
}); });
}); });
@@ -451,22 +450,9 @@ describe('EditGroupView', () => {
expect(elements[0]).toBeVisible(); expect(elements[0]).toBeVisible();
}); });
it('should include associated environment IDs in update payload', async () => { it('should NOT include AssociatedEndpoints in update payload (backend preserves associations)', async () => {
let requestBody: DefaultBodyType; let requestBody: DefaultBodyType;
const associatedEnvs = [
{
...mockEnvironment,
Id: 100,
Name: 'Env 100',
} as Partial<Environment>,
{
...mockEnvironment,
Id: 200,
Name: 'Env 200',
} as Partial<Environment>,
];
server.use( server.use(
http.put('/api/endpoint_groups/:id', async ({ request }) => { http.put('/api/endpoint_groups/:id', async ({ request }) => {
requestBody = await request.json(); requestBody = await request.json();
@@ -475,9 +461,7 @@ describe('EditGroupView', () => {
); );
const user = userEvent.setup(); const user = userEvent.setup();
renderEditGroupView({ renderEditGroupView();
associatedEnvironments: associatedEnvs,
});
// Wait for form to populate // Wait for form to populate
const nameInput = await screen.findByLabelText(/Name/i); const nameInput = await screen.findByLabelText(/Name/i);
@@ -495,11 +479,9 @@ describe('EditGroupView', () => {
await user.click(submitButton); await user.click(submitButton);
// Verify the associated environments are included in payload // Verify AssociatedEndpoints is absent — backend nil-check preserves existing memberships
await waitFor(() => { await waitFor(() => {
expect(requestBody).toMatchObject({ expect(requestBody).not.toHaveProperty('AssociatedEndpoints');
AssociatedEndpoints: [100, 200],
});
}); });
}); });
}); });
@@ -2,8 +2,6 @@ import { useRouter } from '@uirouter/react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { FormikHelpers } from 'formik'; import { FormikHelpers } from 'formik';
import { useEnvironmentList } from '@/react/portainer/environments/queries';
import { notifySuccess } from '@/portainer/services/notifications';
import { useIdParam } from '@/react/hooks/useIdParam'; import { useIdParam } from '@/react/hooks/useIdParam';
import { Widget } from '@@/Widget'; import { Widget } from '@@/Widget';
@@ -13,32 +11,22 @@ import { Alert } from '@@/Alert';
import { useGroup } from '../queries/useGroup'; import { useGroup } from '../queries/useGroup';
import { useUpdateGroupMutation } from '../queries/useUpdateGroupMutation'; import { useUpdateGroupMutation } from '../queries/useUpdateGroupMutation';
import { GroupForm, GroupFormValues } from '../components/GroupForm'; import { GroupForm, GroupFormValues } from '../components/GroupForm';
import { AssociatedEnvironmentsSelector } from '../components/AssociatedEnvironmentsSelector/AssociatedEnvironmentsSelector';
export function EditGroupView() { export function EditGroupView() {
const groupId = useIdParam(); const groupId = useIdParam();
const router = useRouter(); const router = useRouter();
const groupQuery = useGroup(groupId); const groupQuery = useGroup(groupId);
const updateMutation = useUpdateGroupMutation();
// Fetch associated environments for this group (not for unassigned group)
const isUnassignedGroup = groupId === 1; const isUnassignedGroup = groupId === 1;
const environmentsQuery = useEnvironmentList( const updateMutation = useUpdateGroupMutation();
{ groupIds: [groupId], pageLimit: 0 },
{ enabled: !!groupId && !isUnassignedGroup }
);
const isLoading =
groupQuery.isLoading || (!isUnassignedGroup && environmentsQuery.isLoading);
const initialValues: GroupFormValues = useMemo( const initialValues: GroupFormValues = useMemo(
() => ({ () => ({
name: groupQuery.data?.Name ?? '', name: groupQuery.data?.Name ?? '',
description: groupQuery.data?.Description ?? '', description: groupQuery.data?.Description ?? '',
tagIds: groupQuery.data?.TagIds ?? [], tagIds: groupQuery.data?.TagIds ?? [],
associatedEnvironments:
environmentsQuery.environments?.map((e) => e.Id) ?? [],
}), }),
[groupQuery.data, environmentsQuery.environments] [groupQuery.data]
); );
return ( return (
@@ -54,7 +42,7 @@ export function EditGroupView() {
<div className="row"> <div className="row">
<div className="col-sm-12"> <div className="col-sm-12">
<Widget> <Widget>
<Widget.Body loading={isLoading}> <Widget.Body loading={groupQuery.isLoading}>
{groupQuery.isError && ( {groupQuery.isError && (
<Alert color="error" title="Error"> <Alert color="error" title="Error">
Failed to load group details Failed to load group details
@@ -73,6 +61,15 @@ export function EditGroupView() {
</Widget> </Widget>
</div> </div>
</div> </div>
<div className="row pb-20">
<div className="col-sm-12">
<AssociatedEnvironmentsSelector
groupId={groupId}
readOnly={isUnassignedGroup}
/>
</div>
</div>
</> </>
); );
@@ -86,12 +83,11 @@ export function EditGroupView() {
name: values.name, name: values.name,
description: values.description, description: values.description,
tagIds: values.tagIds, tagIds: values.tagIds,
associatedEnvironments: values.associatedEnvironments, // associatedEnvironments omitted — backend preserves existing when field is absent (nil)
}, },
{ {
onSuccess() { onSuccess() {
resetForm(); resetForm();
notifySuccess('Success', 'Group successfully updated');
router.stateService.go('portainer.groups'); router.stateService.go('portainer.groups');
}, },
} }
@@ -0,0 +1,181 @@
import clsx from 'clsx';
import { useState } from 'react';
import { createColumnHelper } from '@tanstack/react-table';
import { truncate } from 'lodash';
import { useEnvironmentList } from '@/react/portainer/environments/queries';
import { isSortType } from '@/react/portainer/environments/queries/useEnvironmentList';
import {
EnvironmentId,
EnvironmentGroupId,
} from '@/react/portainer/environments/types';
import { Datatable } from '@@/datatables';
import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
import { withControlledSelected } from '@@/datatables/extend-options/withControlledSelected';
import { TableRow } from '@@/datatables/TableRow';
import { Sheet, SheetContent, SheetClose, SheetHeader } from '@@/Sheet';
import { Button, LoadingButton } from '@@/buttons';
import { EnvironmentTableData } from './types';
const columnHelper = createColumnHelper<EnvironmentTableData>();
const columns = [
columnHelper.accessor('Name', {
header: 'Name',
id: 'Name',
cell: ({ getValue }) => (
<span title={getValue()}>{truncate(getValue(), { length: 64 })}</span>
),
}),
];
interface Props {
open: boolean;
onClose(): void;
/** IDs already in the group — excluded from the available list. Use for create-form contexts where no group ID exists yet. */
excludeIds?: EnvironmentId[];
/** Endpoint group IDs whose members are excluded from the available list. Prefer this over excludeIds when a group ID is available, to avoid sending thousands of individual IDs in the URL. */
excludeGroupIds?: EnvironmentGroupId[];
/** Called with the full env objects so callers can display names or extract IDs.
* Returns true if the add was committed, false if the user cancelled. */
onAdd:
| ((envs: EnvironmentTableData[]) => Promise<boolean>)
| ((envs: EnvironmentTableData[]) => void);
/** Loading state from the parent — disables buttons and shows spinner */
isLoading?: boolean;
}
export function AddEnvironmentsDrawer({
open,
onClose,
excludeIds,
excludeGroupIds,
onAdd,
isLoading,
}: Props) {
const tableState = useTableStateWithoutStorage('Name');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [selectedEnvs, setSelectedEnvs] = useState<EnvironmentTableData[]>([]);
const [page, setPage] = useState(0);
const {
environments,
totalCount,
isLoading: isEnvsLoading,
} = useEnvironmentList({
pageLimit: tableState.pageSize,
page: page + 1,
search: tableState.search,
sort: isSortType(tableState.sortBy?.id) ? tableState.sortBy.id : 'Name',
order: tableState.sortBy?.desc ? 'desc' : 'asc',
groupIds: [1],
excludeIds,
excludeGroupIds,
});
function handleSelectionChange(ids: string[]) {
setSelectedIds(ids);
const currentDataMap = new Map(
(environments ?? []).map((env) => [String(env.Id), env])
);
setSelectedEnvs((prev) => {
const prevMap = new Map(prev.map((e) => [String(e.Id), e]));
// Keep already-tracked envs that remain selected
const kept = prev.filter((e) => ids.includes(String(e.Id)));
// Add newly selected envs from the current page
const added = ids
.filter((id) => !prevMap.has(id) && currentDataMap.has(id))
.map((id) => currentDataMap.get(id)!);
return [...kept, ...added];
});
}
async function handleAdd() {
const committed = await onAdd(selectedEnvs);
// Close only if the add was committed or there was no confirmation needed
if (committed || committed === undefined) {
resetSelection();
onClose();
}
}
function resetSelection() {
setSelectedIds([]);
setSelectedEnvs([]);
}
return (
<Sheet
open={open}
onOpenChange={(isOpen) => {
if (!isOpen) {
resetSelection();
onClose();
}
}}
>
<SheetContent className="flex flex-col !p-0">
<div className="flex-1 p-4 overflow-auto">
<SheetHeader title="Add environments" />
<Datatable<EnvironmentTableData>
title="Available environments"
columns={columns}
dataset={environments ?? []}
settingsManager={tableState}
isLoading={isEnvsLoading}
isServerSidePagination
page={page}
onPageChange={setPage}
totalCount={totalCount}
getRowId={(row) => String(row.Id)}
renderRow={(row) => (
<TableRow<EnvironmentTableData>
cells={row.getVisibleCells()}
onClick={() => row.toggleSelected()}
className={clsx({ active: row.getIsSelected() })}
aria-selected={row.getIsSelected()}
/>
)}
extendTableOptions={withControlledSelected(
handleSelectionChange,
selectedIds
)}
data-cy="add-environments-drawer-table"
/>
</div>
{/* Don't use StickyFooter here. StickyFooter has classes for the menu to the left that we don't want here */}
<div
className={clsx(
'bottom-0 left-0 right-0 w-full z-50 h-16 sticky justify-end gap-4',
'flex items-center px-6',
'bg-[var(--bg-widget-color)] border-t border-[var(--border-widget-color)]',
'shadow-[0_-2px_5px_rgba(0,0,0,0.1)]'
)}
>
<SheetClose asChild>
<Button
color="default"
disabled={isLoading}
data-cy="add-environments-cancel-button"
size="medium"
>
Cancel
</Button>
</SheetClose>
<LoadingButton
onClick={handleAdd}
disabled={selectedIds.length === 0 || isLoading}
isLoading={!!isLoading}
loadingText="Adding..."
data-cy="add-environments-confirm-button"
size="medium"
>
Confirm
</LoadingButton>
</div>
</SheetContent>
</Sheet>
);
}
@@ -1,4 +1,4 @@
import { render, screen, waitFor, within } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw'; import { http, HttpResponse } from 'msw';
import { vi } from 'vitest'; import { vi } from 'vitest';
@@ -11,227 +11,204 @@ import {
EnvironmentId, EnvironmentId,
} from '@/react/portainer/environments/types'; } from '@/react/portainer/environments/types';
import { EnvironmentGroup } from '../../types';
import { AssociatedEnvironmentsSelector } from './AssociatedEnvironmentsSelector'; import { AssociatedEnvironmentsSelector } from './AssociatedEnvironmentsSelector';
function createEnv(id: EnvironmentId, name: string): Environment { vi.mock('@@/modals/confirm', () => ({
return createMockEnvironment({ Id: id, Name: name, GroupId: 1 }); openConfirm: vi.fn().mockResolvedValue(true),
confirmDelete: vi.fn().mockResolvedValue(true),
}));
const mockGroup: EnvironmentGroup = {
Id: 2,
Name: 'Test Group',
Description: '',
TagIds: [],
};
function createEnv(id: EnvironmentId, name: string): Partial<Environment> {
return createMockEnvironment({ Id: id, Name: name });
} }
function setupMockServer(environments: Array<Environment> = []) { function setupMockServer({
associatedEnvs = [] as Array<Partial<Environment>>,
availableEnvs = [] as Array<Partial<Environment>>,
onPut = undefined as ((body: unknown) => void) | undefined,
} = {}) {
server.use( server.use(
http.get('/api/endpoints', () => http.get('/api/endpoint_groups/2', () => HttpResponse.json(mockGroup)),
HttpResponse.json(environments, { http.get('/api/endpoints', ({ request }) => {
headers: { const url = new URL(request.url);
'x-total-count': String(environments.length), const groupIds = [
'x-total-available': String(environments.length), ...url.searchParams.getAll('groupIds'),
}, ...url.searchParams.getAll('groupIds[]'),
}) ];
)
function makeResponse(envs: Array<Partial<Environment>>) {
return HttpResponse.json(envs, {
headers: {
'x-total-count': String(envs.length),
'x-total-available': String(envs.length),
},
});
}
if (groupIds.includes('2')) return makeResponse(associatedEnvs);
if (groupIds.includes('1')) return makeResponse(availableEnvs);
return makeResponse([]);
}),
http.put('/api/endpoint_groups/2', async ({ request }) => {
const body = await request.json();
onPut?.(body);
return HttpResponse.json(mockGroup);
})
); );
} }
function renderComponent({ function renderComponent(groupId = 2) {
associatedEnvironmentIds = [] as Array<EnvironmentId>,
initialAssociatedEnvironmentIds = [] as Array<EnvironmentId>,
onChange = vi.fn(),
}: {
associatedEnvironmentIds?: Array<EnvironmentId>;
initialAssociatedEnvironmentIds?: Array<EnvironmentId>;
onChange?: (ids: Array<EnvironmentId>) => void;
} = {}) {
const Wrapped = withTestQueryProvider(() => ( const Wrapped = withTestQueryProvider(() => (
<AssociatedEnvironmentsSelector <AssociatedEnvironmentsSelector groupId={groupId} readOnly={false} />
associatedEnvironmentIds={associatedEnvironmentIds}
initialAssociatedEnvironmentIds={initialAssociatedEnvironmentIds}
onChange={onChange}
/>
)); ));
return { return render(<Wrapped />);
...render(<Wrapped />),
onChange,
};
} }
describe('AssociatedEnvironmentsSelector', () => { describe('AssociatedEnvironmentsSelector', () => {
describe('Rendering', () => { describe('Rendering', () => {
it('should render both Available and Associated environments tables', async () => { it('renders the associated environments table', async () => {
setupMockServer(); setupMockServer();
renderComponent(); renderComponent();
expect( expect(
screen.getByRole('heading', { name: 'Available environments' }) await screen.findByRole('heading', { name: 'Associated environments' })
).toBeVisible();
expect(
screen.getByRole('heading', { name: 'Associated environments' })
).toBeVisible(); ).toBeVisible();
}); });
it('should render instruction text', async () => { it('renders an Add button', async () => {
setupMockServer(); setupMockServer();
renderComponent(); renderComponent();
expect( expect(
await screen.findByText(/click on any environment entry to move it/i) await screen.findByTestId('add-environments-button')
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it('should render Associated environments table with data-cy attribute', async () => { it('renders a Remove button that is initially disabled', async () => {
setupMockServer(); setupMockServer({ associatedEnvs: [createEnv(10, 'my-env')] });
renderComponent(); renderComponent();
await waitFor(() => { await screen.findByText('my-env');
expect(screen.getByTestId('group-associatedEndpoints')).toBeVisible();
}); const removeBtn = screen.getByTestId('remove-environments-button');
expect(removeBtn).toBeDisabled();
}); });
it('should display initially associated environments in Associated table', async () => { it('displays environments returned by the API', async () => {
const envs = [createEnv(10, 'associated-env-1')]; setupMockServer({ associatedEnvs: [createEnv(10, 'env-alpha')] });
renderComponent();
setupMockServer(envs); expect(await screen.findByText('env-alpha')).toBeInTheDocument();
renderComponent({
associatedEnvironmentIds: [10],
initialAssociatedEnvironmentIds: [10],
});
await waitFor(() => {
expect(screen.getByText('associated-env-1')).toBeInTheDocument();
});
});
});
describe('Adding environments', () => {
it('should call onChange with new environment ID when clicking an available environment', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const envs = [createEnv(1, 'available-env')];
setupMockServer(envs);
renderComponent({ onChange });
const envRow = await screen.findByText('available-env');
await user.click(envRow);
expect(onChange).toHaveBeenCalledWith([1]);
});
it('should append new environment to existing associated IDs', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const envs = [createEnv(1, 'available-env'), createEnv(10, 'existing')];
setupMockServer(envs);
renderComponent({
associatedEnvironmentIds: [10],
initialAssociatedEnvironmentIds: [10],
onChange,
});
// Wait for the available table to be ready and find the row
const availableTable = await screen.findByTestId(
'group-availableEndpoints'
);
await within(availableTable).findByText('available-env');
// Find the row element that contains the text and click it
const rows = within(availableTable).getAllByRole('row');
const envRow = rows.find(
(row) => row.textContent?.includes('available-env')
);
expect(envRow).toBeDefined();
await user.click(envRow!);
// Wait for onChange to be called with the new environment ID appended
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith([10, 1]);
});
}); });
}); });
describe('Removing environments', () => { describe('Removing environments', () => {
it('should call onChange without the removed environment ID', async () => { it('enables Remove button when a row is selected', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const onChange = vi.fn(); setupMockServer({ associatedEnvs: [createEnv(10, 'env-to-remove')] });
renderComponent();
const envs = [ await screen.findByText('env-to-remove');
createEnv(10, 'associated-env-1'),
createEnv(11, 'associated-env-2'),
];
setupMockServer(envs);
renderComponent({ // First checkbox is the select-all header, second is the first row
associatedEnvironmentIds: [10, 11], const checkboxes = screen.getAllByRole('checkbox');
initialAssociatedEnvironmentIds: [10, 11], await user.click(checkboxes[1]);
onChange,
await waitFor(() => {
expect(screen.getByTestId('remove-environments-button')).toBeEnabled();
}); });
// Wait for initial query to load and row to appear in Associated table, then click
const associatedTable = await screen.findByTestId(
'group-associatedEndpoints'
);
const envRow =
await within(associatedTable).findByText('associated-env-1');
await user.click(envRow);
expect(onChange).toHaveBeenCalledWith([11]);
}); });
it('should call onChange with empty array when removing last environment', async () => { it('calls PUT with filtered environment IDs after confirming remove', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const onChange = vi.fn(); let requestBody: unknown;
const envs = [createEnv(10, 'only-env')]; setupMockServer({
setupMockServer(envs); associatedEnvs: [createEnv(10, 'env-a'), createEnv(11, 'env-b')],
onPut: (body) => {
renderComponent({ requestBody = body;
associatedEnvironmentIds: [10], },
initialAssociatedEnvironmentIds: [10],
onChange,
}); });
renderComponent();
const associatedTable = await screen.findByTestId( await screen.findByText('env-a');
'group-associatedEndpoints'
);
const envRow = await within(associatedTable).findByText('only-env');
await user.click(envRow);
expect(onChange).toHaveBeenCalledWith([]); // Select first row checkbox
const checkboxes = screen.getAllByRole('checkbox');
await user.click(checkboxes[1]);
const removeBtn = await screen.findByTestId('remove-environments-button');
await waitFor(() => expect(removeBtn).toBeEnabled());
await user.click(removeBtn);
await waitFor(() => {
expect(requestBody).toMatchObject({
AssociatedEndpoints: [11],
});
});
}); });
}); });
describe('Computed values', () => { describe('Adding environments (drawer)', () => {
it('should identify added IDs (current but not initial)', () => { it('opens the drawer when Add button is clicked', async () => {
// addedIds = associatedEnvironmentIds.filter(id => !initialAssociatedEnvironmentIds.includes(id)) const user = userEvent.setup();
// When current=[1,2,3] and initial=[2,3], added=[1] setupMockServer({ availableEnvs: [createEnv(20, 'available-env')] });
setupMockServer(); renderComponent();
// This test validates the component's internal logic by checking the highlightIds const addBtn = await screen.findByTestId('add-environments-button');
// passed to AssociatedEnvironmentsTable (newly added envs get "Unsaved" badge) await user.click(addBtn);
renderComponent({
associatedEnvironmentIds: [1, 2, 3],
initialAssociatedEnvironmentIds: [2, 3],
});
// The component will compute addedIds=[1] internally expect(await screen.findByText('Add environments')).toBeVisible();
// We can't directly test internal state, but we verify it renders
expect(screen.getByTestId('group-associatedEndpoints')).toBeVisible();
}); });
it('should identify removed IDs (initial but not current)', () => { it('calls PUT with merged IDs when environments are added from drawer', async () => {
// removedIds = initialAssociatedEnvironmentIds.filter(id => !associatedEnvironmentIds.includes(id)) const user = userEvent.setup();
// When current=[2,3] and initial=[1,2,3], removed=[1] let requestBody: unknown;
setupMockServer();
renderComponent({ setupMockServer({
associatedEnvironmentIds: [2, 3], associatedEnvs: [createEnv(10, 'existing-env')],
initialAssociatedEnvironmentIds: [1, 2, 3], availableEnvs: [createEnv(20, 'new-env')],
onPut: (body) => {
requestBody = body;
},
}); });
renderComponent();
// The component will compute removedIds=[1] internally // Open drawer
// and pass it as includeIds to AvailableEnvironmentsTable const addBtn = await screen.findByTestId('add-environments-button');
expect(screen.getByTestId('group-availableEndpoints')).toBeVisible(); await user.click(addBtn);
// Wait for drawer to open and available env to appear
await screen.findByText('Add environments');
await screen.findByText('new-env');
// Select the available env — find the drawer's checkboxes
// The drawer table has its own checkboxes after the main table ones
const allCheckboxes = screen.getAllByRole('checkbox');
// Last checkbox belongs to the drawer table row
await user.click(allCheckboxes[allCheckboxes.length - 1]);
// Click the Add button in the drawer footer
const confirmAddBtn = screen.getByTestId(
'add-environments-confirm-button'
);
await user.click(confirmAddBtn);
await waitFor(() => {
expect(requestBody).toMatchObject({
AssociatedEndpoints: expect.arrayContaining([10, 20]),
});
});
}); });
}); });
}); });
@@ -1,126 +1,94 @@
import { useMemo, useState } from 'react'; import { useState } from 'react';
import { useEnvironmentList } from '@/react/portainer/environments/queries'; import { useEnvironmentList } from '@/react/portainer/environments/queries';
import { EnvironmentGroupId } from '@/react/portainer/environments/types';
import { FormSection } from '@@/form-components/FormSection'; import { openConfirm } from '@@/modals/confirm';
import { buildConfirmButton } from '@@/modals/utils';
import { Environment, EnvironmentId } from '../../../types'; import { useGroup } from '../../queries/useGroup';
import { useUpdateGroupMutation } from '../../queries/useUpdateGroupMutation';
import { EnvironmentTableData } from './types'; import { EnvironmentTableData } from './types';
import { AssociatedEnvironmentsTable } from './AssociatedEnvironmentsTable'; import { AssociatedEnvironmentsTable } from './AssociatedEnvironmentsTable';
import { AvailableEnvironmentsTable } from './AvailableEnvironmentsTable'; import { AddEnvironmentsDrawer } from './AddEnvironmentsDrawer';
interface Props { interface Props {
/** Group ID when editing an existing group */ groupId: EnvironmentGroupId;
groupId?: number; /* For unassigned group, don't show the add/remove buttons and hide the checkbox */
/** IDs of currently associated environments */ readOnly: boolean;
associatedEnvironmentIds: Array<EnvironmentId>;
/** IDs of initially associated environments for tracking unsaved changes */
initialAssociatedEnvironmentIds: Array<EnvironmentId>;
/** Called when environment IDs change */
onChange: (ids: Array<EnvironmentId>) => void;
} }
export function AssociatedEnvironmentsSelector({ export function AssociatedEnvironmentsSelector({ groupId, readOnly }: Props) {
groupId, const [drawerOpen, setDrawerOpen] = useState(false);
associatedEnvironmentIds,
initialAssociatedEnvironmentIds,
onChange,
}: Props) {
// Track full environment objects for display (populated when clicking rows)
const [environmentCache, setEnvironmentCache] = useState<
Map<EnvironmentId, EnvironmentTableData>
>(new Map());
// Fetch initially associated environments to populate the cache const groupQuery = useGroup(groupId);
const initialEnvsQuery = useEnvironmentList( const environmentsQuery = useEnvironmentList({
groupId groupIds: [groupId],
? { pageLimit: 0,
groupIds: [groupId], });
pageLimit: 0, const updateMutation = useUpdateGroupMutation();
}
: {
endpointIds: initialAssociatedEnvironmentIds,
},
{
enabled: groupId
? groupId !== 1
: initialAssociatedEnvironmentIds.length > 0,
}
);
const environmentMap = useMemo( const currentEnvironments = environmentsQuery.environments ?? [];
() => buildEnvironmentMap(environmentCache, initialEnvsQuery.environments), const currentIds = currentEnvironments.map((e) => e.Id);
[environmentCache, initialEnvsQuery.environments]
);
const associatedSet = new Set(associatedEnvironmentIds);
const initialSet = new Set(initialAssociatedEnvironmentIds);
const addedIds = associatedEnvironmentIds.filter((id) => !initialSet.has(id));
const removedIds = initialAssociatedEnvironmentIds.filter(
(id) => !associatedSet.has(id)
);
const excludeIdsForAvailableEnvironments = groupId
? addedIds
: associatedEnvironmentIds;
const associatedEnvironments = associatedEnvironmentIds
.map((id) => environmentMap.get(id))
.filter((env): env is Environment => env !== undefined);
return ( return (
<FormSection title="Associated environments"> <>
<div className="small text-muted"> <AssociatedEnvironmentsTable
You can select which environment should be part of this group by moving title="Associated environments"
them to the associated environments table. Simply click on any environments={currentEnvironments}
environment entry to move it from one table to the other. isLoading={environmentsQuery.isLoading}
</div> onRemove={handleRemove}
onOpenAddDrawer={() => setDrawerOpen(true)}
isRemoving={updateMutation.isLoading}
data-cy="group-associatedEndpoints"
readOnly={readOnly}
/>
<div className="flex mt-4 gap-5 items-stretch"> <AddEnvironmentsDrawer
<div className="w-1/2 flex flex-col"> open={drawerOpen}
<AvailableEnvironmentsTable onClose={() => setDrawerOpen(false)}
title="Available environments" excludeGroupIds={[groupId]}
excludeIds={excludeIdsForAvailableEnvironments} onAdd={handleAdd}
includeIds={removedIds} isLoading={updateMutation.isLoading}
highlightIds={removedIds} />
onClickRow={handleAddEnvironment} </>
data-cy="group-availableEndpoints"
/>
</div>
<div className="w-1/2 flex flex-col">
<AssociatedEnvironmentsTable
title="Associated environments"
environments={associatedEnvironments}
highlightIds={addedIds}
onClickRow={handleRemoveEnvironment}
data-cy="group-associatedEndpoints"
/>
</div>
</div>
</FormSection>
); );
function handleAddEnvironment(env: EnvironmentTableData) { function handleRemove(selected: EnvironmentTableData[]) {
if (!associatedEnvironmentIds.includes(env.Id)) { const selectedIds = new Set(selected.map((e) => e.Id));
setEnvironmentCache((prev) => new Map(prev).set(env.Id, env)); const remainingIds = currentIds.filter((id) => !selectedIds.has(id));
onChange([...associatedEnvironmentIds, env.Id]);
} updateMutation.mutate({
id: groupId,
name: groupQuery.data?.Name ?? '',
description: groupQuery.data?.Description,
tagIds: groupQuery.data?.TagIds,
associatedEnvironments: remainingIds,
});
} }
function handleRemoveEnvironment(env: EnvironmentTableData) { async function handleAdd(newEnvs: EnvironmentTableData[]): Promise<boolean> {
onChange(associatedEnvironmentIds.filter((id) => id !== env.Id)); const confirmed = await openConfirm({
title: 'Are you sure?',
message: `Are you sure you want to add the selected environment(s) to this group?`,
confirmButton: buildConfirmButton('Add'),
});
if (!confirmed) return false;
const mergedIds = [
...new Set([...currentIds, ...newEnvs.map((e) => e.Id)]),
];
updateMutation.mutate({
id: groupId,
name: groupQuery.data?.Name ?? '',
description: groupQuery.data?.Description,
tagIds: groupQuery.data?.TagIds,
associatedEnvironments: mergedIds,
});
return true;
} }
} }
function buildEnvironmentMap(
cache: Map<EnvironmentId, EnvironmentTableData>,
envs: Array<Environment> | undefined
): Map<EnvironmentId, EnvironmentTableData> {
return new Map([
...cache.entries(),
...(envs ?? []).map(
(env) => [env.Id, { Name: env.Name, Id: env.Id }] as const
),
]);
}
@@ -1,15 +1,17 @@
import { createColumnHelper } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table';
import clsx from 'clsx';
import { truncate } from 'lodash'; import { truncate } from 'lodash';
import { useMemo } from 'react'; import { useMemo, useState } from 'react';
import { Plus } from 'lucide-react';
import clsx from 'clsx';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { AutomationTestingProps } from '@/types'; import { AutomationTestingProps } from '@/types';
import { useTableStateWithoutStorage } from '@@/datatables/useTableState'; import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
import { Datatable, TableRow } from '@@/datatables'; import { Datatable } from '@@/datatables';
import { Badge } from '@@/Badge'; import { withControlledSelected } from '@@/datatables/extend-options/withControlledSelected';
import { Widget } from '@@/Widget'; import { TableRow } from '@@/datatables/TableRow';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { Button } from '@@/buttons';
import { EnvironmentTableData } from './types'; import { EnvironmentTableData } from './types';
@@ -18,64 +20,101 @@ const columnHelper = createColumnHelper<EnvironmentTableData>();
interface Props extends AutomationTestingProps { interface Props extends AutomationTestingProps {
title: string; title: string;
environments: Array<EnvironmentTableData>; environments: Array<EnvironmentTableData>;
onClickRow?: (env: EnvironmentTableData) => void; onRemove(selected: EnvironmentTableData[]): void;
highlightIds?: Array<EnvironmentId>; onOpenAddDrawer(): void;
isRemoving?: boolean;
isLoading?: boolean;
/** When false, Remove fires immediately without a confirmation dialog (e.g. create mode) */
confirmRemove?: boolean;
/** When true, don't show the add/remove buttons and hide the checkbox */
readOnly?: boolean;
} }
export function AssociatedEnvironmentsTable({ export function AssociatedEnvironmentsTable({
title, title,
environments, environments,
onClickRow, onRemove,
highlightIds = [], onOpenAddDrawer,
isRemoving,
isLoading,
confirmRemove = true,
readOnly = false,
'data-cy': dataCy, 'data-cy': dataCy,
}: Props) { }: Props) {
const tableState = useTableStateWithoutStorage('Name'); const tableState = useTableStateWithoutStorage('Name');
const columns = useMemo(() => buildColumns(highlightIds), [highlightIds]); const [selectedIds, setSelectedIds] = useState<string[]>([]);
const columns = useMemo(() => buildColumns(), []);
return ( return (
<Widget className="flex-1 flex flex-col"> // avoid padding issues with the widget
<div <div className="-mx-[15px]">
className={clsx( <Datatable<EnvironmentTableData>
'h-full flex flex-col', disableSelect={readOnly}
'[&_section.datatable]:flex-1 [&_section.datatable]:flex [&_section.datatable]:flex-col', isLoading={isLoading}
'[&_.footer]:!mt-auto' title={title}
columns={columns}
settingsManager={tableState}
dataset={environments}
getRowId={(row) => String(row.Id)}
renderRow={(row) => (
<TableRow<EnvironmentTableData>
cells={row.getVisibleCells()}
onClick={() => row.toggleSelected()}
className={clsx({ active: row.getIsSelected() })}
aria-selected={row.getIsSelected()}
/>
)} )}
> extendTableOptions={withControlledSelected(setSelectedIds, selectedIds)}
<Datatable<EnvironmentTableData> renderTableActions={(selectedItems) =>
// noWidget to avoid padding issues with TableContainer readOnly ? null : (
noWidget <>
title={title} {confirmRemove ? (
columns={columns} <DeleteButton
settingsManager={tableState} disabled={selectedItems.length === 0}
dataset={environments} isLoading={isRemoving}
renderRow={(row) => ( confirmMessage="Are you sure you want to remove the selected environment(s) from this group?"
<TableRow<EnvironmentTableData> onConfirmed={() => handleRemove(selectedItems)}
cells={row.getVisibleCells()} data-cy="remove-environments-button"
onClick={onClickRow ? () => onClickRow(row.original) : undefined} type="button"
/> />
)} ) : (
disableSelect <DeleteButton
data-cy={dataCy || 'environment-table'} disabled={selectedItems.length === 0}
/> onClick={() => {
</div> handleRemove(selectedItems);
</Widget> }}
data-cy="remove-environments-button"
type="button"
/>
)}
<Button
icon={Plus}
onClick={onOpenAddDrawer}
data-cy="add-environments-button"
>
Add
</Button>
</>
)
}
data-cy={dataCy || 'environment-table'}
/>
</div>
); );
function handleRemove(selectedItems: EnvironmentTableData[]) {
onRemove(selectedItems);
setSelectedIds([]);
}
} }
function buildColumns(highlightIds: Array<EnvironmentId>) { function buildColumns() {
return [ return [
columnHelper.accessor('Name', { columnHelper.accessor('Name', {
header: 'Name', header: 'Name',
id: 'Name', id: 'Name',
cell: ({ getValue, row }) => ( cell: ({ getValue }) => (
<span className="flex items-center gap-2"> <span title={getValue()}>{truncate(getValue(), { length: 64 })}</span>
<span title={getValue()}>{truncate(getValue(), { length: 64 })}</span>
{highlightIds.includes(row.original.Id) && (
<Badge type="muted" data-cy="unsaved-badge">
Unsaved
</Badge>
)}
</span>
), ),
}), }),
]; ];
@@ -1,176 +0,0 @@
import { createColumnHelper } from '@tanstack/react-table';
import { truncate } from 'lodash';
import { useMemo, useState } from 'react';
import clsx from 'clsx';
import { useEnvironmentList } from '@/react/portainer/environments/queries';
import { isSortType } from '@/react/portainer/environments/queries/useEnvironmentList';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { AutomationTestingProps } from '@/types';
import { semverCompare } from '@/react/common/semver-utils';
import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState';
import { Datatable, TableRow } from '@@/datatables';
import { Badge } from '@@/Badge';
import { Widget } from '@@/Widget';
import { EnvironmentTableData } from './types';
const columnHelper = createColumnHelper<EnvironmentTableData>();
const tableKey = 'available-environments';
const settingsStore = createPersistedStore(tableKey, 'Name');
interface Props extends AutomationTestingProps {
title: string;
/** IDs to exclude from the query (environments already associated) */
excludeIds: Array<EnvironmentId>;
/** IDs to include in the query (e.g., recently removed from associated - will be highlighted) */
includeIds?: Array<EnvironmentId>;
/** IDs to highlight (unsaved badge) */
highlightIds?: Array<EnvironmentId>;
onClickRow?: (env: EnvironmentTableData) => void;
}
export function AvailableEnvironmentsTable({
title,
excludeIds,
includeIds = [],
highlightIds = [],
onClickRow,
'data-cy': dataCy,
}: Props) {
const tableState = useTableState(settingsStore, tableKey);
const [page, setPage] = useState(0);
const columns = useMemo(
() => buildColumns(new Set(highlightIds)),
[highlightIds]
);
// Query unassigned environments (group 1)
const unassignedQuery = useEnvironmentList({
pageLimit: tableState.pageSize,
page: page + 1,
search: tableState.search,
sort: isSortType(tableState.sortBy?.id) ? tableState.sortBy.id : 'Name',
order: tableState.sortBy?.desc ? 'desc' : 'asc',
groupIds: [1],
excludeIds,
});
// Query removed environments by ID (these are still in their original group until saved)
const removedQuery = useEnvironmentList(
{
endpointIds: includeIds,
search: tableState.search,
},
{ enabled: includeIds.length > 0 }
);
// Merge results: removed environments + unassigned environments (deduped)
const { environments, uniqueRemovedCount } = useMemo(() => {
const unassigned = unassignedQuery.environments || [];
const removed =
includeIds.length > 0 ? removedQuery.environments || [] : [];
if (removed.length === 0) {
return { environments: unassigned, uniqueRemovedCount: 0 };
}
const unassignedIds = new Set(unassigned.map((e) => e.Id));
const uniqueRemoved = removed.filter((e) => !unassignedIds.has(e.Id));
// Sort combined results by name to maintain order
const combined = [...uniqueRemoved, ...unassigned];
const isDesc = tableState.sortBy?.desc ?? false;
// useTypeGuard on tableState.sortBy.id to use as a key for sorting
const sortKey = getSortKey(tableState.sortBy?.id);
if (sortKey) {
return {
environments: combined.sort((a, b) => {
const cmp = semverCompare(
a[sortKey].toString(),
b[sortKey].toString()
);
return isDesc ? -cmp : cmp;
}),
uniqueRemovedCount: uniqueRemoved.length,
};
}
return { environments: combined, uniqueRemovedCount: uniqueRemoved.length };
}, [
unassignedQuery.environments,
removedQuery.environments,
includeIds.length,
tableState.sortBy?.desc,
tableState.sortBy?.id,
]);
const totalCount = unassignedQuery.totalCount + uniqueRemovedCount;
return (
<Widget className="flex-1 flex flex-col">
<div
className={clsx(
'h-full flex flex-col',
'[&_section.datatable]:flex-1 [&_section.datatable]:flex [&_section.datatable]:flex-col',
'[&_.footer]:!mt-auto'
)}
>
<Datatable<EnvironmentTableData>
// noWidget to avoid padding issues with TableContainer
noWidget
title={title}
columns={columns}
settingsManager={tableState}
dataset={environments}
isServerSidePagination
page={page}
onPageChange={setPage}
totalCount={totalCount}
renderRow={(row) => (
<TableRow<EnvironmentTableData>
cells={row.getVisibleCells()}
onClick={onClickRow ? () => onClickRow(row.original) : undefined}
/>
)}
disableSelect
data-cy={dataCy || 'available-environments-table'}
/>
</div>
</Widget>
);
}
function buildColumns(highlightIds: Set<EnvironmentId>) {
return [
columnHelper.accessor('Name', {
header: 'Name',
id: 'Name',
cell: ({ getValue, row }) => (
<span className="flex items-center gap-2">
<span title={getValue()}>{truncate(getValue(), { length: 64 })}</span>
{highlightIds.has(row.original.Id) && (
<Badge type="muted" data-cy="unsaved-badge">
Unsaved
</Badge>
)}
</span>
),
}),
];
}
function getSortKey(sortId?: string): keyof EnvironmentTableData | undefined {
if (!sortId) {
return undefined;
}
switch (sortId) {
case 'Name':
return 'Name';
default:
return 'Name';
}
// extend to other keys as needed
}
@@ -0,0 +1,62 @@
import { useState } from 'react';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { FormSection } from '@@/form-components/FormSection';
import { EnvironmentTableData } from './types';
import { AssociatedEnvironmentsTable } from './AssociatedEnvironmentsTable';
import { AddEnvironmentsDrawer } from './AddEnvironmentsDrawer';
interface Props {
selectedIds: EnvironmentId[];
onChange(ids: EnvironmentId[]): void;
}
/**
* Similar to AssociatedEnvironmentsSelector, but instead of making API calls and getting the environment list from the server, it holds the the selected environments and ids in local state.
*
* This is because on create, there is no group to add / remove environments from yet.
*/
export function FormModeEnvironmentsSelector({ selectedIds, onChange }: Props) {
const [drawerOpen, setDrawerOpen] = useState(false);
const [selectedEnvironments, setSelectedEnvironments] = useState<
EnvironmentTableData[]
>([]);
return (
<FormSection title="Associate environments">
<p className="small text-muted">
Assocate environments to this group by clicking the add button below.
</p>
<AssociatedEnvironmentsTable
title="Associated environments"
environments={selectedEnvironments}
onRemove={handleRemove}
onOpenAddDrawer={() => setDrawerOpen(true)}
confirmRemove={false}
data-cy="group-associatedEndpoints"
/>
<AddEnvironmentsDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
excludeIds={selectedIds}
onAdd={handleAdd}
/>
</FormSection>
);
async function handleRemove(toRemove: EnvironmentTableData[]) {
const removeIds = new Set(toRemove.map((env) => env.Id));
setSelectedEnvironments((prev) => prev.filter((e) => !removeIds.has(e.Id)));
onChange(selectedIds.filter((id) => !removeIds.has(id)));
}
function handleAdd(newEnvs: EnvironmentTableData[]) {
const existingIds = new Set(selectedIds);
const toAdd = newEnvs.filter((e) => !existingIds.has(e.Id));
setSelectedEnvironments((prev) => [...prev, ...toAdd]);
onChange([...selectedIds, ...toAdd.map((e) => e.Id)]);
}
}
@@ -19,7 +19,6 @@ function renderGroupForm({
name: '', name: '',
description: '', description: '',
tagIds: [], tagIds: [],
associatedEnvironments: [],
}, },
onSubmit = vi.fn(), onSubmit = vi.fn(),
submitLabel = 'Create', submitLabel = 'Create',
@@ -40,7 +39,7 @@ function renderGroupForm({
{ ID: 2, Name: 'staging' }, { ID: 2, Name: 'staging' },
]) ])
), ),
// Mock environments list for AssociatedEnvironmentsSelector // Mock environments list for AssociatedEnvironmentsSelector and InlineAvailableEnvironmentsTable
http.get('/api/endpoints', () => http.get('/api/endpoints', () =>
HttpResponse.json([], { HttpResponse.json([], {
headers: { headers: {
@@ -48,6 +47,16 @@ function renderGroupForm({
'x-total-available': '0', 'x-total-available': '0',
}, },
}) })
),
// Mock group endpoint for AssociatedEnvironmentsSelector (edit mode)
http.get('/api/endpoint_groups/:id', ({ params }) =>
HttpResponse.json({
Id: Number(params.id),
Name: 'Test Group',
Description: '',
TagIds: [],
Policies: [],
})
) )
); );
@@ -88,48 +97,43 @@ describe('GroupForm', () => {
).toBeVisible(); ).toBeVisible();
}); });
it('should show Associated environments section when groupId is provided (not unassigned group)', async () => { it('should not show Associated environments section when groupId is provided (edit mode)', async () => {
renderGroupForm({ groupId: 2 }); renderGroupForm({ groupId: 2 });
// Wait for form to render
await screen.findByLabelText(/Name/i); await screen.findByLabelText(/Name/i);
// Check for section title using findByRole // In edit mode, environments are managed by AssociatedEnvironmentsSelector component (rendered separately in EditGroupView)
expect( expect(
await screen.findByRole('heading', { name: /Associated environments/i }) screen.queryByRole('heading', { name: /Associated environments/i })
).toBeVisible(); ).not.toBeInTheDocument();
expect(
screen.queryByTestId('add-environments-button')
).not.toBeInTheDocument();
}); });
it('should show Unassociated environments section when groupId is 1 (unassigned group)', async () => { it('should not show environment section when groupId is 1 (unassigned group)', async () => {
renderGroupForm({ groupId: 1 }); renderGroupForm({ groupId: 1 });
// Wait for form to render
await screen.findByLabelText(/Name/i); await screen.findByLabelText(/Name/i);
// Check for section title using findByRole // In edit mode, environments are managed by AssociatedEnvironmentsSelector component (rendered separately)
expect( expect(
await screen.findByRole('heading', { screen.queryByRole('heading', { name: /Associated environments/i })
name: /Unassociated environments/i, ).not.toBeInTheDocument();
}) expect(
).toBeVisible(); screen.queryByTestId('add-environments-button')
).not.toBeInTheDocument();
// Should NOT show "Associated environments" section (exact match to exclude "Unassociated")
const associatedElements = screen.queryAllByText(
/^Associated environments$/i
);
expect(associatedElements).toHaveLength(0);
}); });
it('should show Associated environments section in create mode (no groupId)', async () => { it('should show associated environments table with Add button in create mode (no groupId)', async () => {
renderGroupForm(); renderGroupForm();
// Wait for form to render
await screen.findByLabelText(/Name/i); await screen.findByLabelText(/Name/i);
// Check for section title using findByRole // FormModeEnvironmentsSelector renders AssociatedEnvironmentsTable with an Add button
expect( expect(
await screen.findByRole('heading', { name: /Associated environments/i }) await screen.findByTestId('add-environments-button')
).toBeVisible(); ).toBeInTheDocument();
}); });
}); });
@@ -161,7 +165,6 @@ describe('GroupForm', () => {
name: 'existing-group', name: 'existing-group',
description: '', description: '',
tagIds: [], tagIds: [],
associatedEnvironments: [],
}, },
}); });
@@ -169,7 +172,6 @@ describe('GroupForm', () => {
name: /Create/i, name: /Create/i,
}); });
// Form is valid but not dirty, so should be disabled
expect(submitButton).toBeDisabled(); expect(submitButton).toBeDisabled();
}); });
@@ -235,7 +237,6 @@ describe('GroupForm', () => {
name: 'test-group', name: 'test-group',
description: 'Test description', description: 'Test description',
tagIds: [], tagIds: [],
associatedEnvironments: [],
}), }),
expect.anything() expect.anything()
); );
@@ -244,7 +245,6 @@ describe('GroupForm', () => {
it('should show loading state during submission', async () => { it('should show loading state during submission', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
// Create a promise that we can control
let resolveSubmit: () => void; let resolveSubmit: () => void;
const onSubmit = vi.fn().mockImplementation( const onSubmit = vi.fn().mockImplementation(
() => () =>
@@ -269,14 +269,12 @@ describe('GroupForm', () => {
await user.click(submitButton); await user.click(submitButton);
// Should show loading state
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByRole('button', { name: /Creating.../i }) screen.getByRole('button', { name: /Creating.../i })
).toBeVisible(); ).toBeVisible();
}); });
// Resolve the submission
resolveSubmit!(); resolveSubmit!();
}); });
}); });
@@ -306,7 +304,6 @@ describe('GroupForm', () => {
name: 'pre-filled-name', name: 'pre-filled-name',
description: 'pre-filled-description', description: 'pre-filled-description',
tagIds: [], tagIds: [],
associatedEnvironments: [],
}, },
}); });
@@ -9,26 +9,28 @@ import { object, string, array, number } from 'yup';
import { useRef } from 'react'; import { useRef } from 'react';
import { TagId } from '@/portainer/tags/types'; import { TagId } from '@/portainer/tags/types';
import {
EnvironmentId,
EnvironmentGroupId,
} from '@/react/portainer/environments/types';
import { useIsPureAdmin } from '@/react/hooks/useUser'; import { useIsPureAdmin } from '@/react/hooks/useUser';
import { useCanExit } from '@/react/hooks/useCanExit'; import { useCanExit } from '@/react/hooks/useCanExit';
import { FormControl } from '@@/form-components/FormControl'; import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input'; import { Input } from '@@/form-components/Input';
import { FormSection } from '@@/form-components/FormSection';
import { TagSelector } from '@@/TagSelector'; import { TagSelector } from '@@/TagSelector';
import { confirmGenericDiscard } from '@@/modals/confirm'; import { confirmGenericDiscard } from '@@/modals/confirm';
import { FormActions } from '@@/form-components/FormActions'; import { LoadingButton } from '@@/buttons';
import { StickyFooter } from '@@/StickyFooter/StickyFooter';
import { EnvironmentGroupId, EnvironmentId } from '../../types'; import { FormModeEnvironmentsSelector } from './AssociatedEnvironmentsSelector/FormModeEnvironmentsSelector';
import { AssociatedEnvironmentsSelector } from './AssociatedEnvironmentsSelector/AssociatedEnvironmentsSelector';
import { AvailableEnvironmentsTable } from './AssociatedEnvironmentsSelector/AvailableEnvironmentsTable';
export interface GroupFormValues { export interface GroupFormValues {
name: string; name: string;
description: string; description: string;
tagIds: Array<TagId>; tagIds: Array<TagId>;
associatedEnvironments: Array<EnvironmentId>; /** Used in create mode only — undefined in edit mode */
associatedEnvironments?: Array<EnvironmentId>;
} }
interface Props { interface Props {
@@ -48,7 +50,6 @@ const validationSchema = object({
name: string().required('Name is required'), name: string().required('Name is required'),
description: string(), description: string(),
tagIds: array(number()), tagIds: array(number()),
associatedEnvironments: array(),
}); });
export function GroupForm({ export function GroupForm({
@@ -71,7 +72,6 @@ export function GroupForm({
enableReinitialize enableReinitialize
> >
<InnerForm <InnerForm
initialValues={initialValues}
submitLabel={submitLabel} submitLabel={submitLabel}
submitLoadingLabel={submitLoadingLabel} submitLoadingLabel={submitLoadingLabel}
groupId={groupId} groupId={groupId}
@@ -81,20 +81,17 @@ export function GroupForm({
} }
interface InnerFormProps { interface InnerFormProps {
initialValues: GroupFormValues;
submitLabel: string; submitLabel: string;
submitLoadingLabel: string; submitLoadingLabel: string;
groupId?: EnvironmentGroupId; groupId?: EnvironmentGroupId;
} }
function InnerForm({ function InnerForm({
initialValues,
submitLabel, submitLabel,
submitLoadingLabel, submitLoadingLabel,
groupId, groupId,
}: InnerFormProps) { }: InnerFormProps) {
const isPureAdmin = useIsPureAdmin(); const isPureAdmin = useIsPureAdmin();
const isUnassignedGroup = groupId === 1;
const { const {
values, values,
errors, errors,
@@ -104,6 +101,7 @@ function InnerForm({
dirty, dirty,
isSubmitting, isSubmitting,
} = useFormikContext<GroupFormValues>(); } = useFormikContext<GroupFormValues>();
const isCreateMode = !groupId;
return ( return (
<Form className="form-horizontal"> <Form className="form-horizontal">
@@ -140,31 +138,25 @@ function InnerForm({
allowCreate={isPureAdmin} allowCreate={isPureAdmin}
/> />
{isUnassignedGroup ? ( {isCreateMode && (
<FormSection title="Unassociated environments"> // Same UI as edit mode, but updates form values instead of the API
<AvailableEnvironmentsTable <FormModeEnvironmentsSelector
title="Unassociated environments" selectedIds={values.associatedEnvironments ?? []}
excludeIds={[]}
data-cy="group-unassociatedEndpoints"
/>
</FormSection>
) : (
<AssociatedEnvironmentsSelector
groupId={groupId}
associatedEnvironmentIds={values.associatedEnvironments}
initialAssociatedEnvironmentIds={initialValues.associatedEnvironments}
onChange={(ids) => setFieldValue('associatedEnvironments', ids)} onChange={(ids) => setFieldValue('associatedEnvironments', ids)}
/> />
)} )}
<FormActions <StickyFooter className="justify-end gap-4">
submitLabel={submitLabel} <LoadingButton
loadingText={submitLoadingLabel} size="medium"
isLoading={isSubmitting} loadingText={submitLoadingLabel}
isValid={isValid && !isSubmitting && dirty} isLoading={isSubmitting}
errors={errors} disabled={!isValid || isSubmitting || !dirty}
data-cy="group-submit-button" data-cy="group-submit-button"
/> >
{submitLabel}
</LoadingButton>
</StickyFooter>
</Form> </Form>
); );
} }
@@ -2,8 +2,9 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios'; import axios, { parseAxiosError } from '@/portainer/services/axios';
import { TagId } from '@/portainer/tags/types'; import { TagId } from '@/portainer/tags/types';
import { withGlobalError, withInvalidate } from '@/react-tools/react-query'; import { withGlobalError } from '@/react-tools/react-query';
import { environmentQueryKeys } from '@/react/portainer/environments/queries/query-keys'; import { environmentQueryKeys } from '@/react/portainer/environments/queries/query-keys';
import { notifySuccess } from '@/portainer/services/notifications';
import { EnvironmentGroupId, EnvironmentId } from '../../types'; import { EnvironmentGroupId, EnvironmentId } from '../../types';
import { EnvironmentGroup } from '../types'; import { EnvironmentGroup } from '../types';
@@ -44,10 +45,11 @@ export function useUpdateGroupMutation() {
return useMutation({ return useMutation({
mutationFn: updateGroup, mutationFn: updateGroup,
onSuccess: () => {
queryClient.invalidateQueries(queryKeys.base());
queryClient.invalidateQueries(environmentQueryKeys.base());
notifySuccess('Success', 'Group successfully updated');
},
...withGlobalError('Failed to update group'), ...withGlobalError('Failed to update group'),
...withInvalidate(queryClient, [
queryKeys.base(),
environmentQueryKeys.base(),
]),
}); });
} }
@@ -36,6 +36,7 @@ export interface BaseEnvironmentsQueryParams {
tagIds?: TagId[]; tagIds?: TagId[];
endpointIds?: EnvironmentId[]; endpointIds?: EnvironmentId[];
excludeIds?: EnvironmentId[]; excludeIds?: EnvironmentId[];
excludeGroupIds?: EnvironmentGroupId[];
tagsPartialMatch?: boolean; tagsPartialMatch?: boolean;
groupIds?: EnvironmentGroupId[]; groupIds?: EnvironmentGroupId[];
status?: EnvironmentStatus[]; status?: EnvironmentStatus[];
@@ -14,6 +14,10 @@ import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector'; import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector';
import { BadgeIcon } from '@@/BadgeIcon'; import { BadgeIcon } from '@@/BadgeIcon';
import { Alert } from '@@/Alert'; import { Alert } from '@@/Alert';
import { FormSection } from '@@/form-components/FormSection';
import { Badge } from '@@/Badge';
import { ExternalLink } from '@@/ExternalLink';
import { useDocsUrl } from '@@/PageHeader/ContextHelp';
import { AnalyticsStateKey } from '../types'; import { AnalyticsStateKey } from '../types';
import { EdgeAgentTab } from '../shared/EdgeAgentTab'; import { EdgeAgentTab } from '../shared/EdgeAgentTab';
@@ -27,50 +31,78 @@ interface Props {
isDockerStandalone?: boolean; isDockerStandalone?: boolean;
} }
const options: BoxSelectorOption< type CreationType =
'agent' | 'api' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync' | 'agent'
>[] = _.compact([ | 'api'
| 'socket'
| 'edgeAgentStandard'
| 'edgeAgentAsync';
const primaryOptions: BoxSelectorOption<CreationType>[] = _.compact([
{ {
id: 'edgeAgentStandard', id: 'edgeAgentStandard',
icon: <BadgeIcon icon={EdgeAgentStandardIcon} size="3xl" />, icon: <BadgeIcon icon={EdgeAgentStandardIcon} size="3xl" />,
label: 'Edge Agent Standard', label: 'Edge Agent Standard',
description: '', description: (
<>
<span>
<Badge type="infoSecondary">Recommended</Badge>{' '}
<Badge type="infoSecondary">Supports Policies</Badge>
</span>
<span className="mt-1 block">
The remote environment will initiate connections to the Portainer
server, with the ability to open a secure on-demand tunnel for
real-time interaction. The Portainer server must be accessible from
the Edge Agent environment.
</span>
</>
),
value: 'edgeAgentStandard', value: 'edgeAgentStandard',
}, },
isBE && { isBE && {
id: 'edgeAgentAsync', id: 'edgeAgentAsync',
icon: <BadgeIcon icon={EdgeAgentAsyncIcon} size="3xl" />, icon: <BadgeIcon icon={EdgeAgentAsyncIcon} size="3xl" />,
label: 'Edge Agent Async', label: 'Edge Agent Async',
description: '', description:
value: 'edgeAgentAsync', 'The remote environment will initiate connections to the Portainer server, without the ability to open a real-time tunnel. The Portainer server must be accessible from the Edge Agent environment.',
value: 'edgeAgentAsync' as CreationType,
}, },
]);
const legacyOptions: BoxSelectorOption<CreationType>[] = [
{ {
id: 'agent', id: 'agent',
icon: <BadgeIcon icon={Zap} size="3xl" />, icon: <BadgeIcon icon={Zap} size="3xl" />,
label: 'Agent', label: 'Agent',
description: '', description:
'The Portainer Server will initiate connections to the remote environment. The agent on the remote environment must be accessible from the Portainer server environment.',
value: 'agent', value: 'agent',
}, },
{ {
id: 'api', id: 'api',
icon: <BadgeIcon icon={Network} size="3xl" />, icon: <BadgeIcon icon={Network} size="3xl" />,
label: 'API', label: 'API',
description: '', description: 'Connect to the environment directly via the Docker API.',
value: 'api', value: 'api',
}, },
{ {
id: 'socket', id: 'socket',
icon: <BadgeIcon icon={Plug2} size="3xl" />, icon: <BadgeIcon icon={Plug2} size="3xl" />,
label: 'Socket', label: 'Socket',
description: '', description: 'Connect to the environment directly via the Docker socket.',
value: 'socket', value: 'socket',
}, },
]); ];
const containerEngine = ContainerEngine.Docker; const containerEngine = ContainerEngine.Docker;
export function WizardDocker({ onCreate, isDockerStandalone }: Props) { export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
const [creationType, setCreationType] = useState(options[0].value); const edgeAgentDocsUrl = useDocsUrl(
'/faqs/getting-started/why-do-we-recommend-using-the-edge-agent-instead-of-the-traditional-agent'
);
const [creationType, setCreationType] = useState<CreationType>(
primaryOptions[0].value
);
const tab = getTab(creationType); const tab = getTab(creationType);
@@ -89,23 +121,43 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
)} )}
<BoxSelector <BoxSelector
onChange={(v) => setCreationType(v)} onChange={(v) => setCreationType(v)}
options={options} options={primaryOptions}
value={creationType} value={creationType}
radioName="creation-type" radioName="creation-type"
className="!-mb-2"
/> />
<FormSection
key="legacy-options"
title="More options"
titleSize="sm"
isFoldable
defaultFolded={false}
className="[&>label]:mb-5"
>
<p className="text-xs text-muted mb-2">
These are legacy options that don&apos;t support edge features or
policy management. For most use cases,{' '}
<ExternalLink
to={edgeAgentDocsUrl}
data-cy="wizard-edge-agent-docs-link"
>
the Edge Agent is recommended
</ExternalLink>
</p>
<BoxSelector
onChange={(v) => setCreationType(v)}
options={legacyOptions}
value={creationType}
radioName="creation-type"
/>
</FormSection>
{tab} {tab}
</div> </div>
); );
function getTab( function getTab(creationType: CreationType) {
creationType:
| 'agent'
| 'api'
| 'socket'
| 'edgeAgentStandard'
| 'edgeAgentAsync'
) {
switch (creationType) { switch (creationType) {
case 'agent': case 'agent':
return ( return (
@@ -3,6 +3,7 @@ import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { withTestRouter } from '@/react/test-utils/withRouter';
import { server, http } from '@/setup-tests/server'; import { server, http } from '@/setup-tests/server';
import { WizardKubernetes } from './WizardKubernetes'; import { WizardKubernetes } from './WizardKubernetes';
@@ -37,9 +38,10 @@ function renderComponent() {
) )
); );
const Wrapped = withTestQueryProvider(() => ( const WithRouter = withTestRouter(() => (
<WizardKubernetes onCreate={() => {}} /> <WizardKubernetes onCreate={() => {}} />
)); ));
const Wrapped = withTestQueryProvider(WithRouter);
return render(<Wrapped />); return render(<Wrapped />);
} }
@@ -15,6 +15,10 @@ import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
import { BoxSelectorOption } from '@@/BoxSelector/types'; import { BoxSelectorOption } from '@@/BoxSelector/types';
import { BoxSelector } from '@@/BoxSelector'; import { BoxSelector } from '@@/BoxSelector';
import { BEOverlay } from '@@/BEFeatureIndicator/BEOverlay'; import { BEOverlay } from '@@/BEFeatureIndicator/BEOverlay';
import { FormSection } from '@@/form-components/FormSection';
import { Badge } from '@@/Badge';
import { ExternalLink } from '@@/ExternalLink';
import { useDocsUrl } from '@@/PageHeader/ContextHelp';
import { AnalyticsStateKey } from '../types'; import { AnalyticsStateKey } from '../types';
import { EdgeAgentTab } from '../shared/EdgeAgentTab'; import { EdgeAgentTab } from '../shared/EdgeAgentTab';
@@ -32,21 +36,26 @@ type CreationType =
| 'agent' | 'agent'
| 'kubeconfig'; | 'kubeconfig';
const options: BoxSelectorOption<CreationType>[] = _.compact([ const primaryOptions: BoxSelectorOption<CreationType>[] = _.compact([
{
id: 'agent_endpoint',
icon: Zap,
iconType: 'badge',
label: 'Agent',
value: 'agent',
description: '',
},
{ {
id: 'edgeAgentStandard', id: 'edgeAgentStandard',
icon: EdgeAgentStandardIcon, icon: EdgeAgentStandardIcon,
iconType: 'badge', iconType: 'badge',
label: 'Edge Agent Standard', label: 'Edge Agent Standard',
description: '', description: (
<>
<span>
<Badge type="infoSecondary">Recommended</Badge>{' '}
<Badge type="infoSecondary">Supports Policies</Badge>
</span>
<span className="mt-1 block">
The remote environment will initiate connections to the Portainer
server, with the ability to open a secure on-demand tunnel for
real-time interaction. The Portainer server must be accessible from
the Edge Agent environment.
</span>
</>
),
value: 'edgeAgentStandard', value: 'edgeAgentStandard',
}, },
isBE && { isBE && {
@@ -54,22 +63,40 @@ const options: BoxSelectorOption<CreationType>[] = _.compact([
icon: EdgeAgentAsyncIcon, icon: EdgeAgentAsyncIcon,
iconType: 'badge', iconType: 'badge',
label: 'Edge Agent Async', label: 'Edge Agent Async',
description: '', description:
'The remote environment will initiate connections to the Portainer server, without the ability to open a real-time tunnel. The Portainer server must be accessible from the Edge Agent environment.',
value: 'edgeAgentAsync', value: 'edgeAgentAsync',
}, },
]);
const legacyOptions: BoxSelectorOption<CreationType>[] = [
{
id: 'agent_endpoint',
icon: Zap,
iconType: 'badge',
label: 'Agent',
value: 'agent',
description:
'The Portainer Server will initiate connections to the remote environment. The agent on the remote environment must be accessible from the Portainer server environment.',
},
{ {
id: 'kubeconfig_endpoint', id: 'kubeconfig_endpoint',
icon: UploadCloud, icon: UploadCloud,
iconType: 'badge', iconType: 'badge',
label: 'Import', label: 'Import',
value: 'kubeconfig', value: 'kubeconfig',
description: 'Import an existing Kubernetes config', description: 'Import an existing Kubernetes config.',
feature: FeatureId.K8S_CREATE_FROM_KUBECONFIG, feature: FeatureId.K8S_CREATE_FROM_KUBECONFIG,
}, },
]); ];
export function WizardKubernetes({ onCreate }: Props) { export function WizardKubernetes({ onCreate }: Props) {
const [creationType, setCreationType] = useState(options[0].value); const edgeAgentDocsUrl = useDocsUrl(
'/faqs/getting-started/why-do-we-recommend-using-the-edge-agent-instead-of-the-traditional-agent'
);
const [creationType, setCreationType] = useState<CreationType>(
primaryOptions[0].value
);
const tab = getTab(creationType); const tab = getTab(creationType);
@@ -77,11 +104,38 @@ export function WizardKubernetes({ onCreate }: Props) {
<div className="form-horizontal"> <div className="form-horizontal">
<BoxSelector <BoxSelector
onChange={(v) => setCreationType(v)} onChange={(v) => setCreationType(v)}
options={options} options={primaryOptions}
value={creationType} value={creationType}
radioName="creation-type" radioName="creation-type"
className="!-mb-2"
/> />
<FormSection
key="legacy-options"
title="More options"
titleSize="sm"
isFoldable
defaultFolded={false}
className="[&>label]:mb-5"
>
<p className="text-xs text-muted mb-2">
These are legacy options that don&apos;t support edge features or
policy management. For most use cases,{' '}
<ExternalLink
to={edgeAgentDocsUrl}
data-cy="wizard-edge-agent-docs-link"
>
the Edge Agent is recommended
</ExternalLink>
</p>
<BoxSelector
onChange={(v) => setCreationType(v)}
options={legacyOptions}
value={creationType}
radioName="creation-type"
/>
</FormSection>
{tab} {tab}
</div> </div>
); );
@@ -14,6 +14,10 @@ import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector'; import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector';
import { BadgeIcon } from '@@/BadgeIcon'; import { BadgeIcon } from '@@/BadgeIcon';
import { TextTip } from '@@/Tip/TextTip'; import { TextTip } from '@@/Tip/TextTip';
import { FormSection } from '@@/form-components/FormSection';
import { Badge } from '@@/Badge';
import { ExternalLink } from '@@/ExternalLink';
import { useDocsUrl } from '@@/PageHeader/ContextHelp';
import { AnalyticsStateKey } from '../types'; import { AnalyticsStateKey } from '../types';
import { EdgeAgentTab } from '../shared/EdgeAgentTab'; import { EdgeAgentTab } from '../shared/EdgeAgentTab';
@@ -25,43 +29,66 @@ interface Props {
onCreate(environment: Environment, analytics: AnalyticsStateKey): void; onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
} }
const options: BoxSelectorOption< type CreationType = 'agent' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync';
'agent' | 'api' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync'
>[] = _.compact([ const primaryOptions: BoxSelectorOption<CreationType>[] = _.compact([
{ {
id: 'edgeAgentStandard', id: 'edgeAgentStandard',
icon: <BadgeIcon icon={EdgeAgentStandardIcon} size="3xl" />, icon: <BadgeIcon icon={EdgeAgentStandardIcon} size="3xl" />,
label: 'Edge Agent Standard', label: 'Edge Agent Standard',
description: '', description: (
<>
<span>
<Badge type="infoSecondary">Recommended</Badge>{' '}
<Badge type="infoSecondary">Supports Policies</Badge>
</span>
<span className="mt-1 block">
The remote environment will initiate connections to the Portainer
server, with the ability to open a secure on-demand tunnel for
real-time interaction. The Portainer server must be accessible from
the Edge Agent environment.
</span>
</>
),
value: 'edgeAgentStandard', value: 'edgeAgentStandard',
}, },
isBE && { isBE && {
id: 'edgeAgentAsync', id: 'edgeAgentAsync',
icon: <BadgeIcon icon={EdgeAgentAsyncIcon} size="3xl" />, icon: <BadgeIcon icon={EdgeAgentAsyncIcon} size="3xl" />,
label: 'Edge Agent Async', label: 'Edge Agent Async',
description: '', description:
'The remote environment will initiate connections to the Portainer server, without the ability to open a real-time tunnel. The Portainer server must be accessible from the Edge Agent environment.',
value: 'edgeAgentAsync', value: 'edgeAgentAsync',
}, },
]);
const legacyOptions: BoxSelectorOption<CreationType>[] = [
{ {
id: 'agent', id: 'agent',
icon: <BadgeIcon icon={Zap} size="3xl" />, icon: <BadgeIcon icon={Zap} size="3xl" />,
label: 'Agent', label: 'Agent',
description: '', description:
'The Portainer Server will initiate connections to the remote environment. The agent on the remote environment must be accessible from the Portainer server environment.',
value: 'agent', value: 'agent',
}, },
{ {
id: 'socket', id: 'socket',
icon: <BadgeIcon icon={Plug2} size="3xl" />, icon: <BadgeIcon icon={Plug2} size="3xl" />,
label: 'Socket', label: 'Socket',
description: '', description: 'Connect to the environment directly via the Docker socket.',
value: 'socket', value: 'socket',
}, },
]); ];
const containerEngine = ContainerEngine.Podman; const containerEngine = ContainerEngine.Podman;
export function WizardPodman({ onCreate }: Props) { export function WizardPodman({ onCreate }: Props) {
const [creationType, setCreationType] = useState(options[0].value); const edgeAgentDocsUrl = useDocsUrl(
'/faqs/getting-started/why-do-we-recommend-using-the-edge-agent-instead-of-the-traditional-agent'
);
const [creationType, setCreationType] = useState<CreationType>(
primaryOptions[0].value
);
const tab = getTab(creationType); const tab = getTab(creationType);
@@ -69,10 +96,38 @@ export function WizardPodman({ onCreate }: Props) {
<div className="form-horizontal"> <div className="form-horizontal">
<BoxSelector <BoxSelector
onChange={(v) => setCreationType(v)} onChange={(v) => setCreationType(v)}
options={options} options={primaryOptions}
value={creationType} value={creationType}
radioName="creation-type" radioName="creation-type"
className="!-mb-2"
/> />
<FormSection
key="legacy-options"
title="More options"
titleSize="sm"
isFoldable
defaultFolded={false}
className="[&>label]:mb-5"
>
<p className="text-xs text-muted mb-2">
These are legacy options that don&apos;t support edge features or
policy management. For most use cases,{' '}
<ExternalLink
to={edgeAgentDocsUrl}
data-cy="wizard-edge-agent-docs-link"
>
the Edge Agent is recommended
</ExternalLink>
</p>
<BoxSelector
onChange={(v) => setCreationType(v)}
options={legacyOptions}
value={creationType}
radioName="creation-type"
/>
</FormSection>
<TextTip color="orange" className="mb-2" inline={false}> <TextTip color="orange" className="mb-2" inline={false}>
Currently, Portainer only supports <b>Podman 5</b> running in rootful Currently, Portainer only supports <b>Podman 5</b> running in rootful
(privileged) mode on <b>CentOS 9</b> Linux environments. Rootless mode (privileged) mode on <b>CentOS 9</b> Linux environments. Rootless mode
@@ -82,14 +137,7 @@ export function WizardPodman({ onCreate }: Props) {
</div> </div>
); );
function getTab( function getTab(creationType: CreationType) {
creationType:
| 'agent'
| 'api'
| 'socket'
| 'edgeAgentStandard'
| 'edgeAgentAsync'
) {
switch (creationType) { switch (creationType) {
case 'agent': case 'agent':
return ( return (
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"docker": "v29.2.1" "docker": "v29.3.0"
} }
+66 -69
View File
@@ -1,6 +1,6 @@
module github.com/portainer/portainer module github.com/portainer/portainer
go 1.25.7 go 1.25.8
replace github.com/robfig/cron/v3 => github.com/robfig/cron/v3 v3.0.1 // Not actively maintained. Pinned to last known good version. Review needed when upgrading. replace github.com/robfig/cron/v3 => github.com/robfig/cron/v3 v3.0.1 // Not actively maintained. Pinned to last known good version. Review needed when upgrading.
@@ -16,7 +16,7 @@ require (
github.com/aws/smithy-go v1.20.3 github.com/aws/smithy-go v1.20.3
github.com/cbroglie/mustache v1.4.0 github.com/cbroglie/mustache v1.4.0
github.com/compose-spec/compose-go/v2 v2.9.1 github.com/compose-spec/compose-go/v2 v2.9.1
github.com/containerd/containerd v1.7.29 github.com/containerd/containerd v1.7.30
github.com/coreos/go-semver v0.3.1 github.com/coreos/go-semver v0.3.1
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5
github.com/docker/cli v28.5.1+incompatible github.com/docker/cli v28.5.1+incompatible
@@ -24,7 +24,7 @@ require (
github.com/docker/docker v28.5.1+incompatible github.com/docker/docker v28.5.1+incompatible
github.com/fvbommel/sortorder v1.1.0 github.com/fvbommel/sortorder v1.1.0
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
github.com/go-git/go-git/v5 v5.16.4 github.com/go-git/go-git/v5 v5.17.0
github.com/go-ldap/ldap/v3 v3.4.1 github.com/go-ldap/ldap/v3 v3.4.1
github.com/gofrs/uuid v4.2.0+incompatible github.com/gofrs/uuid v4.2.0+incompatible
github.com/golang-jwt/jwt/v4 v4.5.2 github.com/golang-jwt/jwt/v4 v4.5.2
@@ -54,20 +54,20 @@ require (
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
go.podman.io/image/v5 v5.37.0 go.podman.io/image/v5 v5.37.0
go.yaml.in/yaml/v3 v3.0.4 go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.45.0 golang.org/x/crypto v0.46.0
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/mod v0.29.0 golang.org/x/mod v0.31.0
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.18.0 golang.org/x/sync v0.19.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.18.5 helm.sh/helm/v3 v3.20.0
k8s.io/api v0.33.3 k8s.io/api v0.35.0
k8s.io/apimachinery v0.33.4 k8s.io/apimachinery v0.35.0
k8s.io/cli-runtime v0.33.3 k8s.io/cli-runtime v0.35.0
k8s.io/client-go v0.33.3 k8s.io/client-go v0.35.0
k8s.io/kubectl v0.33.3 k8s.io/kubectl v0.35.0
k8s.io/kubelet v0.33.2 k8s.io/kubelet v0.35.0
k8s.io/metrics v0.33.3 k8s.io/metrics v0.35.0
oras.land/oras-go/v2 v2.6.0 oras.land/oras-go/v2 v2.6.0
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
) )
@@ -79,14 +79,14 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
github.com/BurntSushi/toml v1.5.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
@@ -112,7 +112,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudflare/cfssl v1.6.4 // indirect github.com/cloudflare/cfssl v1.6.4 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/containerd/console v1.0.5 // indirect github.com/containerd/console v1.0.5 // indirect
github.com/containerd/containerd/api v1.9.0 // indirect github.com/containerd/containerd/api v1.9.0 // indirect
github.com/containerd/containerd/v2 v2.1.5 // indirect github.com/containerd/containerd/v2 v2.1.5 // indirect
@@ -125,7 +125,7 @@ require (
github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/containers/ocicrypt v1.2.1 // indirect github.com/containers/ocicrypt v1.2.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/buildx v0.29.1 // indirect github.com/docker/buildx v0.29.1 // indirect
@@ -137,40 +137,40 @@ require (
github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fatih/camelcase v1.0.0 // indirect github.com/fatih/camelcase v1.0.0 // indirect
github.com/fatih/color v1.15.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsevents v0.2.0 // indirect github.com/fsnotify/fsevents v0.2.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
github.com/go-errors/errors v1.4.2 // indirect github.com/go-errors/errors v1.5.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-billy/v5 v5.8.0 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/flock v0.13.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.6.9 // indirect github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gosuri/uitable v0.0.4 // indirect github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -194,7 +194,7 @@ require (
github.com/lib/pq v1.10.9 // indirect github.com/lib/pq v1.10.9 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/lithammer/dedent v1.1.0 // indirect github.com/lithammer/dedent v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
@@ -222,7 +222,7 @@ require (
github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/mschoch/smat v0.2.0 // indirect github.com/mschoch/smat v0.2.0 // indirect
@@ -235,24 +235,24 @@ require (
github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rubenv/sql-migrate v1.8.0 // indirect github.com/rubenv/sql-migrate v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/asm v1.1.3 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cobra v1.10.1 // indirect github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
@@ -265,58 +265,55 @@ require (
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect
github.com/zclconf/go-cty v1.17.0 // indirect github.com/zclconf/go-cty v1.17.0 // indirect
github.com/zmap/zcrypto v0.0.0-20241123155728-2916694fa469 // indirect github.com/zmap/zcrypto v0.0.0-20241123155728-2916694fa469 // indirect
github.com/zmap/zlint/v3 v3.6.4 // indirect github.com/zmap/zlint/v3 v3.6.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect go.opentelemetry.io/otel/sdk v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.podman.io/storage v1.60.0 // indirect go.podman.io/storage v1.60.0 // indirect
go.uber.org/mock v0.6.0 // indirect go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.37.0 // indirect golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/time v0.12.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.74.2 // indirect google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.33.3 // indirect k8s.io/apiextensions-apiserver v0.35.0 // indirect
k8s.io/apiserver v0.33.3 // indirect k8s.io/apiserver v0.35.0 // indirect
k8s.io/component-base v0.33.3 // indirect k8s.io/component-base v0.35.0 // indirect
k8s.io/component-helpers v0.33.3 // indirect k8s.io/component-helpers v0.35.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kustomize/api v0.19.0 // indirect sigs.k8s.io/kustomize/api v0.20.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect sigs.k8s.io/kustomize/kyaml v0.21.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.5.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect
tags.cncf.io/container-device-interface v1.0.1 // indirect tags.cncf.io/container-device-interface v1.0.1 // indirect
) )
+144 -156
View File
@@ -1,4 +1,6 @@
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
@@ -12,8 +14,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg6
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28= github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
@@ -38,8 +40,8 @@ github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/RoaringBitmap/roaring/v2 v2.5.0 h1:TJ45qCM7D7fIEBwKd9zhoR0/S1egfnSSIzLU1e1eYLY= github.com/RoaringBitmap/roaring/v2 v2.5.0 h1:TJ45qCM7D7fIEBwKd9zhoR0/S1egfnSSIzLU1e1eYLY=
github.com/RoaringBitmap/roaring/v2 v2.5.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= github.com/RoaringBitmap/roaring/v2 v2.5.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
@@ -133,8 +135,8 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON
github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8= github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0= github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.9.1 h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs= github.com/compose-spec/compose-go/v2 v2.9.1 h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs=
@@ -144,8 +146,8 @@ github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJ
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE=
github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M=
github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0=
github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI=
github.com/containerd/containerd/v2 v2.1.5 h1:pWSmPxUszaLZKQPvOx27iD4iH+aM6o0BoN9+hg77cro= github.com/containerd/containerd/v2 v2.1.5 h1:pWSmPxUszaLZKQPvOx27iD4iH+aM6o0BoN9+hg77cro=
@@ -187,8 +189,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -238,8 +240,8 @@ github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJ
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
@@ -249,12 +251,12 @@ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c= github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c=
@@ -264,24 +266,24 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU= github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU=
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI= github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -294,14 +296,12 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
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-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -313,8 +313,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -343,8 +343,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY= github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY=
github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ= github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -379,8 +379,8 @@ github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -459,7 +459,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -482,8 +481,8 @@ github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -557,8 +556,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@@ -576,12 +576,12 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -591,8 +591,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
@@ -623,8 +623,8 @@ github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -633,14 +633,14 @@ github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvM
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
@@ -657,8 +657,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
@@ -669,8 +669,8 @@ github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w= github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU=
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
@@ -694,8 +694,8 @@ github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IEx
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE= github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE=
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -707,8 +707,6 @@ github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -717,9 +715,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
@@ -753,13 +748,6 @@ 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/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
@@ -784,8 +772,8 @@ go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4=
@@ -794,10 +782,10 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=
@@ -822,16 +810,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsu
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.podman.io/image/v5 v5.37.0 h1:yzgQybwuWIIeK63hu+mQqna/wOh96XD5cpVc6j8Dg5M= go.podman.io/image/v5 v5.37.0 h1:yzgQybwuWIIeK63hu+mQqna/wOh96XD5cpVc6j8Dg5M=
@@ -842,8 +830,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -864,8 +852,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -875,8 +863,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -897,11 +885,11 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -915,8 +903,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -951,8 +939,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -964,8 +952,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -980,8 +968,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -992,26 +980,28 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
@@ -1022,8 +1012,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
@@ -1047,52 +1037,50 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
helm.sh/helm/v3 v3.18.5 h1:Cc3Z5vd6kDrZq9wO9KxKLNEickiTho6/H/dBNRVSos4= helm.sh/helm/v3 v3.20.0 h1:2M+0qQwnbI1a2CxN7dbmfsWHg/MloeaFMnZCY56as50=
helm.sh/helm/v3 v3.18.5/go.mod h1:L/dXDR2r539oPlFP1PJqKAC1CUgqHJDLkxKpDGrWnyg= helm.sh/helm/v3 v3.20.0/go.mod h1:rTavWa0lagZOxGfdhu4vgk1OjH2UYCnrDKE2PVC4N0o=
k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s= k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4= k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4=
k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds=
k8s.io/cli-runtime v0.33.3 h1:Dgy4vPjNIu8LMJBSvs8W0LcdV0PX/8aGG1DA1W8lklA= k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE=
k8s.io/cli-runtime v0.33.3/go.mod h1:yklhLklD4vLS8HNGgC9wGiuHWze4g7x6XQZ+8edsKEo= k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY=
k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
k8s.io/component-helpers v0.33.3 h1:fjWVORSQfI0WKzPeIFSju/gMD9sybwXBJ7oPbqQu6eM= k8s.io/component-helpers v0.35.0 h1:wcXv7HJRksgVjM4VlXJ1CNFBpyDHruRI99RrBtrJceA=
k8s.io/component-helpers v0.33.3/go.mod h1:7iwv+Y9Guw6X4RrnNQOyQlXcvJrVjPveHVqUA5dm31c= k8s.io/component-helpers v0.35.0/go.mod h1:ahX0m/LTYmu7fL3W8zYiIwnQ/5gT28Ex4o2pymF63Co=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/kubectl v0.33.3 h1:r/phHvH1iU7gO/l7tTjQk2K01ER7/OAJi8uFHHyWSac= k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc=
k8s.io/kubectl v0.33.3/go.mod h1:euj2bG56L6kUGOE/ckZbCoudPwuj4Kud7BR0GzyNiT0= k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo=
k8s.io/kubelet v0.33.2 h1:wxEau5/563oJb3j3KfrCKlNWWx35YlSgDLOYUBCQ0pg= k8s.io/kubelet v0.35.0 h1:8cgJHCBCKLYuuQ7/Pxb/qWbJfX1LXIw7790ce9xHq7c=
k8s.io/kubelet v0.33.2/go.mod h1:way8VCDTUMiX1HTOvJv7M3xS/xNysJI6qh7TOqMe5KM= k8s.io/kubelet v0.35.0/go.mod h1:ciRzAXn7C4z5iB7FhG1L2CGPPXLTVCABDlbXt/Zz8YA=
k8s.io/metrics v0.33.3 h1:9CcqBz15JZfISqwca33gdHS8I6XfsK1vA8WUdEnG70g= k8s.io/metrics v0.35.0 h1:xVFoqtAGm2dMNJAcB5TFZJPCen0uEqqNt52wW7ABbX8=
k8s.io/metrics v0.33.3/go.mod h1:Aw+cdg4AYHw0HvUY+lCyq40FOO84awrqvJRTw0cmXDs= k8s.io/metrics v0.35.0/go.mod h1:g2Up4dcBygZi2kQSEQVDByFs+VUwepJMzzQLJJLpq4M=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ=
sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4= software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4=
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78/go.mod h1:B7Wf0Ya4DHF9Yw+qfZuJijQYkWicqDa+79Ytmmq3Kjg= software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78/go.mod h1:B7Wf0Ya4DHF9Yw+qfZuJijQYkWicqDa+79Ytmmq3Kjg=
tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc=
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Portainer.io", "author": "Portainer.io",
"name": "@portainer/ce", "name": "@portainer/ce",
"homepage": "http://portainer.io", "homepage": "http://portainer.io",
"version": "2.39.0", "version": "2.39.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@github.com:portainer/portainer.git" "url": "git@github.com:portainer/portainer.git"