Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c1f3c27d5 | ||
|
|
b468160606 | ||
|
|
a42e96b650 | ||
|
|
5a19f66a37 | ||
|
|
b271026188 | ||
|
|
d168e3c912 | ||
|
|
0b6ebd70e0 | ||
|
|
127e03552a | ||
|
|
f2bdfc6eff | ||
|
|
5db67faa00 | ||
|
|
f9dcfcb435 | ||
|
|
1d1bb526d0 | ||
|
|
c8fe8ba4fd | ||
|
|
d3692a5a5f | ||
|
|
3407811c28 | ||
|
|
b71db0d1f1 | ||
|
|
5e5e85ff3a | ||
|
|
65d82e12ee | ||
|
|
d9e730e0a5 | ||
|
|
21eb20b35e | ||
|
|
f85a7ea24c | ||
|
|
6aacb61c87 | ||
|
|
bb2c75ba93 | ||
|
|
16536c8a71 |
@@ -11,7 +11,7 @@ see also:
|
||||
## Package Manager
|
||||
|
||||
- **PNPM** 10+ (for frontend)
|
||||
- **Go** 1.25.7 (for backend)
|
||||
- **Go** 1.25.8 (for backend)
|
||||
|
||||
## Build Commands
|
||||
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.39.0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.39.1",
|
||||
"LDAPSettings": {
|
||||
"AnonymousMode": true,
|
||||
"AutoCreateUsers": true,
|
||||
@@ -942,7 +942,7 @@
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
@@ -2,8 +2,14 @@ package customtemplates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
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"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"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)
|
||||
}
|
||||
|
||||
customTemplate, err := handler.DataStore.CustomTemplate().Read(portainer.CustomTemplateID(customTemplateID))
|
||||
if handler.DataStore.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)
|
||||
var customTemplate *portainer.CustomTemplate
|
||||
if err := handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
customTemplate, err = tx.CustomTemplate().Read(portainer.CustomTemplateID(customTemplateID))
|
||||
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
|
||||
|
||||
115
api/http/handler/customtemplates/customtemplate_file_test.go
Normal file
115
api/http/handler/customtemplates/customtemplate_file_test.go
Normal file
@@ -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
|
||||
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
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)
|
||||
} else if err != nil {
|
||||
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"
|
||||
"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"
|
||||
@@ -20,6 +21,9 @@ func TestInspectHandler(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)
|
||||
|
||||
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}))
|
||||
@@ -42,7 +46,7 @@ func TestInspectHandler(t *testing.T) {
|
||||
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) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/custom_templates/"+templateID, nil)
|
||||
|
||||
@@ -19,6 +19,7 @@ type StackViewModel struct {
|
||||
Name string
|
||||
IsExternal bool
|
||||
Type portainer.StackType
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
// 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,
|
||||
IsExternal: true,
|
||||
Type: portainer.DockerComposeStack,
|
||||
Labels: container.Labels,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +70,7 @@ func GetDockerStacks(tx dataservices.DataStoreTx, securityContext *security.Rest
|
||||
Name: name,
|
||||
IsExternal: true,
|
||||
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,
|
||||
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"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"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/stretchr/testify/assert"
|
||||
@@ -28,12 +28,13 @@ func TestHandler_getDockerStacks(t *testing.T) {
|
||||
containers := []types.Container{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
dockerconsts.ComposeStackNameLabel: "stack1",
|
||||
consts.ComposeStackNameLabel: "stack1",
|
||||
},
|
||||
},
|
||||
{
|
||||
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{
|
||||
Annotations: swarm.Annotations{
|
||||
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(&portainer.Stack{
|
||||
ID: 2,
|
||||
Name: "stack2",
|
||||
Name: "stack2", // stack 2 on env 2
|
||||
EndpointID: 2,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
}))
|
||||
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
|
||||
}))
|
||||
|
||||
// testing admin user
|
||||
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
stacksList, err := GetDockerStacks(tx, &security.RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
@@ -93,11 +96,43 @@ func TestHandler_getDockerStacks(t *testing.T) {
|
||||
Name: "stack2",
|
||||
IsExternal: true,
|
||||
Type: portainer.DockerComposeStack,
|
||||
Labels: map[string]string{
|
||||
consts.ComposeStackNameLabel: "stack2",
|
||||
"io.portainer.accesscontrol.public": "true",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stack3",
|
||||
IsExternal: true,
|
||||
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 endpointIds query []int false "will return only 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 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)"
|
||||
|
||||
@@ -38,6 +38,7 @@ type EnvironmentsQuery struct {
|
||||
edgeStackId portainer.EdgeStackID
|
||||
edgeStackStatus *portainer.EdgeStackStatusType
|
||||
excludeIds []portainer.EndpointID
|
||||
excludeGroupIds []portainer.EndpointGroupID
|
||||
edgeGroupIds []portainer.EdgeGroupID
|
||||
excludeEdgeGroupIds []portainer.EdgeGroupID
|
||||
}
|
||||
@@ -80,6 +81,11 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
excludeGroupIDs, err := getNumberArrayQueryParameter[portainer.EndpointGroupID](r, "excludeGroupIds")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
edgeGroupIDs, err := getNumberArrayQueryParameter[portainer.EdgeGroupID](r, "edgeGroupIds")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
@@ -119,6 +125,7 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||
tagIds: tagIDs,
|
||||
endpointIds: endpointIDs,
|
||||
excludeIds: excludeIDs,
|
||||
excludeGroupIds: excludeGroupIDs,
|
||||
tagsPartialMatch: tagsPartialMatch,
|
||||
groupIds: groupIDs,
|
||||
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 {
|
||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
|
||||
}
|
||||
|
||||
@@ -151,6 +151,46 @@ func Test_Filter_excludeIDs(t *testing.T) {
|
||||
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) {
|
||||
n := 10000
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.39.0
|
||||
// @version 2.39.1
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
@@ -55,29 +57,43 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
|
||||
TTY: true,
|
||||
}, 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(
|
||||
config,
|
||||
"GET", // WebSocket uses GET for the upgrade request
|
||||
req.URL().String(),
|
||||
channelProtocolList...,
|
||||
)
|
||||
if err != nil {
|
||||
exec, err = remotecommand.NewSPDYExecutor(config, "POST", req.URL())
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
if err == nil {
|
||||
err = exec.StreamWithContext(context.TODO(), streamOpts)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("context", "StartExecProcess").
|
||||
Msg("WebSocket exec failed, falling back to SPDY")
|
||||
}
|
||||
|
||||
err = exec.StreamWithContext(context.TODO(), remotecommand.StreamOptions{
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Tty: true,
|
||||
})
|
||||
// Fall back to SPDY executor
|
||||
exec, err = remotecommand.NewSPDYExecutor(config, "POST", req.URL())
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("unable to create SPDY executor: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = exec.StreamWithContext(context.TODO(), streamOpts)
|
||||
if err != nil {
|
||||
var exitError utilexec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
errChan <- errors.New("unable to start exec process")
|
||||
errChan <- fmt.Errorf("unable to start exec process: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ func Test_GenerateYAML(t *testing.T) {
|
||||
name: portainer-ctx
|
||||
current-context: portainer-ctx
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: test-user
|
||||
user:
|
||||
|
||||
@@ -576,7 +576,7 @@ type (
|
||||
}
|
||||
|
||||
PolicyChartBundle struct {
|
||||
PolicyChartSummary
|
||||
PolicyChartSummary `mapstructure:",squash"`
|
||||
EncodedTgz string `json:"EncodedTgz"`
|
||||
Namespace string `json:"Namespace"`
|
||||
PreReleaseManifest string `json:"PreReleaseManifest,omitempty"`
|
||||
@@ -1874,7 +1874,7 @@ type (
|
||||
|
||||
const (
|
||||
// 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)
|
||||
APIVersionSupport = "LTS"
|
||||
// Edition is what this edition of Portainer is called
|
||||
|
||||
@@ -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,
|
||||
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 {
|
||||
|
||||
34
api/stacks/stackutils/env.go
Normal file
34
api/stacks/stackutils/env.go
Normal 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
|
||||
}
|
||||
@@ -1,38 +1,38 @@
|
||||
package stackutils
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"context"
|
||||
"path"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/cli/compose/types"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"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"
|
||||
)
|
||||
|
||||
func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error {
|
||||
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
|
||||
type StackFileValidationConfig struct {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
composeConfigFile := types.ConfigFile{
|
||||
Config: composeConfigYAML,
|
||||
}
|
||||
|
||||
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 _, service := range composeConfig.Services {
|
||||
if !config.SecuritySettings.AllowBindMountsForRegularUsers {
|
||||
for _, volume := range service.Volumes {
|
||||
if volume.Type == "bind" {
|
||||
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")
|
||||
}
|
||||
|
||||
if !securitySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
|
||||
if !config.SecuritySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -65,13 +65,21 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo
|
||||
}
|
||||
|
||||
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) {
|
||||
stackContent, err := fileService.GetFileContent(stack.ProjectPath, file)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package stackutils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -30,32 +31,236 @@ networks:
|
||||
`)
|
||||
|
||||
securitySettings := &portainer.EndpointSecuritySettings{}
|
||||
err := IsValidStackFile(yamlContent, securitySettings)
|
||||
err := IsValidStackFile(StackFileValidationConfig{
|
||||
Content: yamlContent,
|
||||
SecuritySettings: securitySettings,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIsValidStackFile_PortEnv(t *testing.T) {
|
||||
yamlContent := []byte(`
|
||||
// TestIsValidStackFile_MissingEnvVarBehavior documents how port variable position affects
|
||||
// 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"
|
||||
|
||||
services:
|
||||
webservice:
|
||||
api:
|
||||
image: nginx
|
||||
container_name: hello-world
|
||||
networks:
|
||||
- "mynet1"
|
||||
ports:
|
||||
- "${PORT}:80"
|
||||
|
||||
networks:
|
||||
mynet1:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.16.0.0/24
|
||||
`)
|
||||
|
||||
securitySettings := &portainer.EndpointSecuritySettings{}
|
||||
err := IsValidStackFile(yamlContent, securitySettings)
|
||||
- "${API_PORT}:${API_PORT}"
|
||||
`),
|
||||
SecuritySettings: securitySettings,
|
||||
Env: map[string]string{"API_PORT": "3000"},
|
||||
})
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -25,3 +25,24 @@ func StackResourceControlGetter[
|
||||
func StackResourceControlID(endpointID portainer.EndpointID, name string) string {
|
||||
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 > button:focus {
|
||||
@apply text-blue-7;
|
||||
z-index: 3;
|
||||
cursor: default;
|
||||
/* background-color: var(--text-pagination-span-color); */
|
||||
background-color: var(--bg-pagination-color);
|
||||
|
||||
@@ -65,7 +65,7 @@ const SheetOverlay = forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
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
|
||||
)}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
@@ -76,7 +76,7 @@ const SheetOverlay = forwardRef<
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
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: {
|
||||
side: {
|
||||
|
||||
@@ -15,11 +15,10 @@ export function StickyFooter({
|
||||
<div
|
||||
className={clsx(
|
||||
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-10 h-16',
|
||||
'fixed bottom-0 right-0 z-40 h-16',
|
||||
'flex items-center px-6',
|
||||
'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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { ComponentProps, PropsWithChildren, ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
import { confirmDelete } from '@@/modals/confirm';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { LoadingButton } from './LoadingButton';
|
||||
import { Button } from './Button';
|
||||
|
||||
type ConfirmOrClick =
|
||||
| {
|
||||
@@ -26,7 +27,10 @@ export function DeleteButton({
|
||||
size,
|
||||
children,
|
||||
isLoading,
|
||||
text = 'Remove',
|
||||
loadingText = 'Removing...',
|
||||
icon = false,
|
||||
type,
|
||||
'data-cy': dataCy,
|
||||
...props
|
||||
}: PropsWithChildren<
|
||||
@@ -35,7 +39,10 @@ export function DeleteButton({
|
||||
size?: ComponentProps<typeof Button>['size'];
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
text?: string;
|
||||
loadingText?: string;
|
||||
icon?: boolean;
|
||||
type?: ComponentProps<typeof Button>['type'];
|
||||
}
|
||||
>) {
|
||||
if (isLoading === undefined) {
|
||||
@@ -46,10 +53,11 @@ export function DeleteButton({
|
||||
disabled={disabled || isLoading}
|
||||
onClick={() => handleClick()}
|
||||
icon={Trash2}
|
||||
className="!m-0"
|
||||
className={clsx('!m-0', icon ? 'btn-icon' : '')}
|
||||
data-cy={dataCy}
|
||||
type={type}
|
||||
>
|
||||
{children || 'Remove'}
|
||||
{children || text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -65,6 +73,7 @@ export function DeleteButton({
|
||||
data-cy={dataCy}
|
||||
isLoading={isLoading}
|
||||
loadingText={loadingText}
|
||||
type={type}
|
||||
>
|
||||
{children || 'Remove'}
|
||||
</LoadingButton>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { difference } from 'lodash';
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
@@ -14,6 +16,7 @@ import { defaultGetRowId } from './defaultGetRowId';
|
||||
import { Table } from './Table';
|
||||
import { NestedTable } from './NestedTable';
|
||||
import { DatatableContent } from './DatatableContent';
|
||||
import { DatatableFooter } from './DatatableFooter';
|
||||
import { BasicTableSettings, DefaultType } from './types';
|
||||
|
||||
interface Props<D extends DefaultType> extends AutomationTestingProps {
|
||||
@@ -69,6 +72,18 @@ export function NestedDatatable<D extends DefaultType>({
|
||||
...(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 (
|
||||
<NestedTable>
|
||||
<Table.Container noWidget>
|
||||
@@ -80,6 +95,17 @@ export function NestedDatatable<D extends DefaultType>({
|
||||
aria-label={ariaLabel}
|
||||
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>
|
||||
</NestedTable>
|
||||
);
|
||||
|
||||
@@ -7,31 +7,43 @@ interface Props<D extends DefaultType = DefaultType> {
|
||||
cells: Cell<D, unknown>[];
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
'aria-selected'?: boolean;
|
||||
}
|
||||
|
||||
export function TableRow<D extends DefaultType = DefaultType>({
|
||||
cells,
|
||||
className,
|
||||
onClick,
|
||||
'aria-selected': ariaSelected,
|
||||
}: Props<D>) {
|
||||
return (
|
||||
<tr
|
||||
className={clsx(className, { 'cursor-pointer': !!onClick })}
|
||||
onClick={onClick}
|
||||
aria-selected={ariaSelected}
|
||||
>
|
||||
{cells.map((cell) => (
|
||||
<td key={cell.id} className={getClassName(cell.column.columnDef.meta)}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
{cells.map((cell) => {
|
||||
const { className, width } = parseMeta(cell.column.columnDef.meta);
|
||||
return (
|
||||
<td key={cell.id} className={className} style={{ width }}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function getClassName<D extends DefaultType = DefaultType>(
|
||||
function parseMeta<D extends DefaultType = DefaultType>(
|
||||
meta: ColumnMeta<D, unknown> | undefined
|
||||
) {
|
||||
return !!meta && 'className' in meta && typeof meta.className === 'string'
|
||||
? meta.className
|
||||
: '';
|
||||
const 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,
|
||||
meta: {
|
||||
width: 50,
|
||||
width: '50px',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export function FormSection({
|
||||
</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 */}
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,11 +37,11 @@ export function Modal({
|
||||
<Context.Provider value>
|
||||
<DialogOverlay
|
||||
isOpen
|
||||
className={clsx(
|
||||
styles.overlay,
|
||||
'flex items-center justify-center z-50'
|
||||
)}
|
||||
className={clsx(styles.overlay, 'flex items-center justify-center')}
|
||||
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
|
||||
aria-label={ariaLabel}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ContainerDetailsViewModel } from '@/docker/models/containerDetails';
|
||||
import { ResourceControlType } from '@/react/portainer/access-control/types';
|
||||
import { trimContainerName } from '@/docker/filters/utils';
|
||||
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 { PageHeader } from '@@/PageHeader';
|
||||
@@ -32,7 +32,7 @@ export function ItemView() {
|
||||
{ select: (c) => new ContainerDetailsViewModel(c) }
|
||||
);
|
||||
|
||||
const registriesQuery = useRegistries();
|
||||
const registriesQuery = useEnvironmentRegistries(environmentId);
|
||||
|
||||
if (
|
||||
containerQuery.isLoading ||
|
||||
|
||||
@@ -25,6 +25,7 @@ export function TasksDatatable({
|
||||
search={search}
|
||||
aria-label="Tasks table"
|
||||
data-cy="docker-service-tasks-nested-datatable"
|
||||
initialSortBy={{ id: 'Updated', desc: true }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ export function StackEditorTab({
|
||||
envType={envType}
|
||||
schema={schemaQuery.data}
|
||||
versions={versions}
|
||||
isSubmitting={mutation.isLoading}
|
||||
isSaved={mutation.isSuccess}
|
||||
webhookId={webhookId}
|
||||
/>
|
||||
|
||||
@@ -43,6 +43,7 @@ const defaultProps = {
|
||||
schema: { type: 'object' } as JSONSchema7,
|
||||
isOrphaned: false,
|
||||
stackId: 1,
|
||||
isSubmitting: false,
|
||||
isSaved: false,
|
||||
webhookId: '',
|
||||
};
|
||||
@@ -377,17 +378,7 @@ describe('form submission', () => {
|
||||
});
|
||||
|
||||
it('should show loading text during submission', async () => {
|
||||
const onSubmit = vi.fn().mockImplementation(() => new Promise(() => {})); // Never resolves
|
||||
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);
|
||||
renderComponent({ isSubmitting: true }, {});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Deployment in progress.../)).toBeInTheDocument();
|
||||
|
||||
@@ -29,6 +29,7 @@ interface StackEditorTabInnerProps {
|
||||
versions?: Array<number>;
|
||||
stackId: Stack['Id'];
|
||||
isSaved: boolean;
|
||||
isSubmitting: boolean;
|
||||
webhookId: string;
|
||||
}
|
||||
|
||||
@@ -42,20 +43,15 @@ export function StackEditorTabInner({
|
||||
versions,
|
||||
stackId,
|
||||
isSaved,
|
||||
isSubmitting,
|
||||
webhookId,
|
||||
}: StackEditorTabInnerProps) {
|
||||
const { authorized: isAuthorizedToUpdate } = useAuthorizations(
|
||||
'PortainerStackUpdate'
|
||||
);
|
||||
|
||||
const {
|
||||
values,
|
||||
errors,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
initialValues,
|
||||
} = useFormikContext<StackEditorFormValues>();
|
||||
const { values, errors, setFieldValue, isValid, initialValues } =
|
||||
useFormikContext<StackEditorFormValues>();
|
||||
|
||||
usePreventExit(
|
||||
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[] = [
|
||||
{ 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);
|
||||
|
||||
expect(result[0].label).toBe('Unassigned');
|
||||
expect(result[0].options[0]).toEqual({ label: 'Env 1', value: 1 });
|
||||
});
|
||||
|
||||
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'
|
||||
);
|
||||
const result = getEnvironmentOptions(groups, environments);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].label).toBe('Others');
|
||||
expect(result[0].options).toEqual([
|
||||
{ label: 'Env 1', value: 1 },
|
||||
{ label: 'Env 2', value: 2 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
||||
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
||||
@@ -73,7 +74,11 @@ export function getEnvironmentOptions(
|
||||
return acc;
|
||||
}
|
||||
|
||||
const groupId = environment.GroupId;
|
||||
let groupId = environment.GroupId;
|
||||
if (!groups.some((g) => g.Id === groupId)) {
|
||||
groupId = -1;
|
||||
}
|
||||
|
||||
if (!acc[groupId]) {
|
||||
acc[groupId] = [];
|
||||
}
|
||||
@@ -87,13 +92,10 @@ export function getEnvironmentOptions(
|
||||
return Object.entries(groupedEnvironments).map(([groupId, envOptions]) => {
|
||||
const parsedGroupId = parseInt(groupId, 10);
|
||||
const group = groups.find((g) => g.Id === parsedGroupId);
|
||||
if (!group && parsedGroupId !== 1) {
|
||||
throw new Error(`Missing group with id ${groupId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
label: group?.Name || 'Unassigned',
|
||||
options: envOptions,
|
||||
label: group?.Name || 'Others',
|
||||
options: sortBy(envOptions, 'label'),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { Pencil } from 'lucide-react';
|
||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
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 { Widget, WidgetBody } from '@@/Widget';
|
||||
import { AddButton, Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
import { Icon } from '@@/Icon';
|
||||
import { AddButton } from '@@/buttons';
|
||||
|
||||
import { applicationIsKind, isExternalApplication } from '../../utils';
|
||||
import { appStackIdLabel, appStackKindLabel } from '../../constants';
|
||||
@@ -17,6 +14,8 @@ import { useApplicationServices } from '../../queries/useApplicationServices';
|
||||
import { useAppStackFile } from '../../queries/useAppStackFile';
|
||||
import { Application } from '../../types';
|
||||
|
||||
import { EdgeEditButton } from './EdgeEditButton';
|
||||
import { EditButton } from './EditButton';
|
||||
import { RestartApplicationButton } from './RestartApplicationButton';
|
||||
import { RedeployApplicationButton } from './RedeployApplicationButton';
|
||||
import { RollbackApplicationButton } from './RollbackApplicationButton';
|
||||
@@ -42,9 +41,6 @@ export function ApplicationDetailsWidget() {
|
||||
const namespaceData = useNamespaceQuery(environmentId, namespace);
|
||||
const isSystemNamespace = namespaceData.data?.IsSystem;
|
||||
|
||||
// check if user is edge admin
|
||||
const edgeAdminQuery = useIsEdgeAdmin();
|
||||
|
||||
// get app info
|
||||
const { data: app } = useApplication(
|
||||
environmentId,
|
||||
@@ -81,35 +77,15 @@ export function ApplicationDetailsWidget() {
|
||||
{!isSystemNamespace && (
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
<Authorized authorizations="K8sApplicationDetailsW">
|
||||
<Link
|
||||
to={
|
||||
appStackKind === 'edge'
|
||||
? 'edge.stacks.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" />
|
||||
{appStackKind === 'edge' ? (
|
||||
<EdgeEditButton stackId={appStackId} />
|
||||
) : (
|
||||
<EditButton to=".edit">
|
||||
{externalApp
|
||||
? 'Edit external application'
|
||||
: 'Edit this application'}
|
||||
</Button>
|
||||
</Link>
|
||||
</EditButton>
|
||||
)}
|
||||
</Authorized>
|
||||
{!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">
|
||||
<Widget>
|
||||
<Widget.Body>
|
||||
|
||||
@@ -298,14 +298,13 @@ describe('EditGroupView', () => {
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
// Verify the request URL and body
|
||||
// Verify the request URL and body.
|
||||
await waitFor(() => {
|
||||
expect(requestUrl).toBe('/api/endpoint_groups/2');
|
||||
expect(requestBody).toEqual({
|
||||
Name: 'Updated Group',
|
||||
Description: 'Test description',
|
||||
TagIDs: [1],
|
||||
AssociatedEndpoints: [1], // The associated environment ID
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -451,22 +450,9 @@ describe('EditGroupView', () => {
|
||||
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;
|
||||
|
||||
const associatedEnvs = [
|
||||
{
|
||||
...mockEnvironment,
|
||||
Id: 100,
|
||||
Name: 'Env 100',
|
||||
} as Partial<Environment>,
|
||||
{
|
||||
...mockEnvironment,
|
||||
Id: 200,
|
||||
Name: 'Env 200',
|
||||
} as Partial<Environment>,
|
||||
];
|
||||
|
||||
server.use(
|
||||
http.put('/api/endpoint_groups/:id', async ({ request }) => {
|
||||
requestBody = await request.json();
|
||||
@@ -475,9 +461,7 @@ describe('EditGroupView', () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderEditGroupView({
|
||||
associatedEnvironments: associatedEnvs,
|
||||
});
|
||||
renderEditGroupView();
|
||||
|
||||
// Wait for form to populate
|
||||
const nameInput = await screen.findByLabelText(/Name/i);
|
||||
@@ -495,11 +479,9 @@ describe('EditGroupView', () => {
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
// Verify the associated environments are included in payload
|
||||
// Verify AssociatedEndpoints is absent — backend nil-check preserves existing memberships
|
||||
await waitFor(() => {
|
||||
expect(requestBody).toMatchObject({
|
||||
AssociatedEndpoints: [100, 200],
|
||||
});
|
||||
expect(requestBody).not.toHaveProperty('AssociatedEndpoints');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,6 @@ import { useRouter } from '@uirouter/react';
|
||||
import { useMemo } from 'react';
|
||||
import { FormikHelpers } from 'formik';
|
||||
|
||||
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { useIdParam } from '@/react/hooks/useIdParam';
|
||||
|
||||
import { Widget } from '@@/Widget';
|
||||
@@ -13,32 +11,22 @@ import { Alert } from '@@/Alert';
|
||||
import { useGroup } from '../queries/useGroup';
|
||||
import { useUpdateGroupMutation } from '../queries/useUpdateGroupMutation';
|
||||
import { GroupForm, GroupFormValues } from '../components/GroupForm';
|
||||
import { AssociatedEnvironmentsSelector } from '../components/AssociatedEnvironmentsSelector/AssociatedEnvironmentsSelector';
|
||||
|
||||
export function EditGroupView() {
|
||||
const groupId = useIdParam();
|
||||
const router = useRouter();
|
||||
const groupQuery = useGroup(groupId);
|
||||
const updateMutation = useUpdateGroupMutation();
|
||||
|
||||
// Fetch associated environments for this group (not for unassigned group)
|
||||
const isUnassignedGroup = groupId === 1;
|
||||
const environmentsQuery = useEnvironmentList(
|
||||
{ groupIds: [groupId], pageLimit: 0 },
|
||||
{ enabled: !!groupId && !isUnassignedGroup }
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
groupQuery.isLoading || (!isUnassignedGroup && environmentsQuery.isLoading);
|
||||
const updateMutation = useUpdateGroupMutation();
|
||||
|
||||
const initialValues: GroupFormValues = useMemo(
|
||||
() => ({
|
||||
name: groupQuery.data?.Name ?? '',
|
||||
description: groupQuery.data?.Description ?? '',
|
||||
tagIds: groupQuery.data?.TagIds ?? [],
|
||||
associatedEnvironments:
|
||||
environmentsQuery.environments?.map((e) => e.Id) ?? [],
|
||||
}),
|
||||
[groupQuery.data, environmentsQuery.environments]
|
||||
[groupQuery.data]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -54,7 +42,7 @@ export function EditGroupView() {
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<Widget.Body loading={isLoading}>
|
||||
<Widget.Body loading={groupQuery.isLoading}>
|
||||
{groupQuery.isError && (
|
||||
<Alert color="error" title="Error">
|
||||
Failed to load group details
|
||||
@@ -73,6 +61,15 @@ export function EditGroupView() {
|
||||
</Widget>
|
||||
</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,
|
||||
description: values.description,
|
||||
tagIds: values.tagIds,
|
||||
associatedEnvironments: values.associatedEnvironments,
|
||||
// associatedEnvironments omitted — backend preserves existing when field is absent (nil)
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
resetForm();
|
||||
notifySuccess('Success', 'Group successfully updated');
|
||||
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 { http, HttpResponse } from 'msw';
|
||||
import { vi } from 'vitest';
|
||||
@@ -11,227 +11,204 @@ import {
|
||||
EnvironmentId,
|
||||
} from '@/react/portainer/environments/types';
|
||||
|
||||
import { EnvironmentGroup } from '../../types';
|
||||
|
||||
import { AssociatedEnvironmentsSelector } from './AssociatedEnvironmentsSelector';
|
||||
|
||||
function createEnv(id: EnvironmentId, name: string): Environment {
|
||||
return createMockEnvironment({ Id: id, Name: name, GroupId: 1 });
|
||||
vi.mock('@@/modals/confirm', () => ({
|
||||
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(
|
||||
http.get('/api/endpoints', () =>
|
||||
HttpResponse.json(environments, {
|
||||
headers: {
|
||||
'x-total-count': String(environments.length),
|
||||
'x-total-available': String(environments.length),
|
||||
},
|
||||
})
|
||||
)
|
||||
http.get('/api/endpoint_groups/2', () => HttpResponse.json(mockGroup)),
|
||||
http.get('/api/endpoints', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const groupIds = [
|
||||
...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({
|
||||
associatedEnvironmentIds = [] as Array<EnvironmentId>,
|
||||
initialAssociatedEnvironmentIds = [] as Array<EnvironmentId>,
|
||||
onChange = vi.fn(),
|
||||
}: {
|
||||
associatedEnvironmentIds?: Array<EnvironmentId>;
|
||||
initialAssociatedEnvironmentIds?: Array<EnvironmentId>;
|
||||
onChange?: (ids: Array<EnvironmentId>) => void;
|
||||
} = {}) {
|
||||
function renderComponent(groupId = 2) {
|
||||
const Wrapped = withTestQueryProvider(() => (
|
||||
<AssociatedEnvironmentsSelector
|
||||
associatedEnvironmentIds={associatedEnvironmentIds}
|
||||
initialAssociatedEnvironmentIds={initialAssociatedEnvironmentIds}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<AssociatedEnvironmentsSelector groupId={groupId} readOnly={false} />
|
||||
));
|
||||
|
||||
return {
|
||||
...render(<Wrapped />),
|
||||
onChange,
|
||||
};
|
||||
return render(<Wrapped />);
|
||||
}
|
||||
|
||||
describe('AssociatedEnvironmentsSelector', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render both Available and Associated environments tables', async () => {
|
||||
it('renders the associated environments table', async () => {
|
||||
setupMockServer();
|
||||
renderComponent();
|
||||
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Available environments' })
|
||||
).toBeVisible();
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Associated environments' })
|
||||
await screen.findByRole('heading', { name: 'Associated environments' })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render instruction text', async () => {
|
||||
it('renders an Add button', async () => {
|
||||
setupMockServer();
|
||||
renderComponent();
|
||||
|
||||
expect(
|
||||
await screen.findByText(/click on any environment entry to move it/i)
|
||||
await screen.findByTestId('add-environments-button')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render Associated environments table with data-cy attribute', async () => {
|
||||
setupMockServer();
|
||||
it('renders a Remove button that is initially disabled', async () => {
|
||||
setupMockServer({ associatedEnvs: [createEnv(10, 'my-env')] });
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('group-associatedEndpoints')).toBeVisible();
|
||||
});
|
||||
await screen.findByText('my-env');
|
||||
|
||||
const removeBtn = screen.getByTestId('remove-environments-button');
|
||||
expect(removeBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should display initially associated environments in Associated table', async () => {
|
||||
const envs = [createEnv(10, 'associated-env-1')];
|
||||
it('displays environments returned by the API', async () => {
|
||||
setupMockServer({ associatedEnvs: [createEnv(10, 'env-alpha')] });
|
||||
renderComponent();
|
||||
|
||||
setupMockServer(envs);
|
||||
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]);
|
||||
});
|
||||
expect(await screen.findByText('env-alpha')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
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 onChange = vi.fn();
|
||||
setupMockServer({ associatedEnvs: [createEnv(10, 'env-to-remove')] });
|
||||
renderComponent();
|
||||
|
||||
const envs = [
|
||||
createEnv(10, 'associated-env-1'),
|
||||
createEnv(11, 'associated-env-2'),
|
||||
];
|
||||
setupMockServer(envs);
|
||||
await screen.findByText('env-to-remove');
|
||||
|
||||
renderComponent({
|
||||
associatedEnvironmentIds: [10, 11],
|
||||
initialAssociatedEnvironmentIds: [10, 11],
|
||||
onChange,
|
||||
// First checkbox is the select-all header, second is the first row
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
await user.click(checkboxes[1]);
|
||||
|
||||
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 onChange = vi.fn();
|
||||
let requestBody: unknown;
|
||||
|
||||
const envs = [createEnv(10, 'only-env')];
|
||||
setupMockServer(envs);
|
||||
|
||||
renderComponent({
|
||||
associatedEnvironmentIds: [10],
|
||||
initialAssociatedEnvironmentIds: [10],
|
||||
onChange,
|
||||
setupMockServer({
|
||||
associatedEnvs: [createEnv(10, 'env-a'), createEnv(11, 'env-b')],
|
||||
onPut: (body) => {
|
||||
requestBody = body;
|
||||
},
|
||||
});
|
||||
renderComponent();
|
||||
|
||||
const associatedTable = await screen.findByTestId(
|
||||
'group-associatedEndpoints'
|
||||
);
|
||||
const envRow = await within(associatedTable).findByText('only-env');
|
||||
await user.click(envRow);
|
||||
await screen.findByText('env-a');
|
||||
|
||||
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', () => {
|
||||
it('should identify added IDs (current but not initial)', () => {
|
||||
// addedIds = associatedEnvironmentIds.filter(id => !initialAssociatedEnvironmentIds.includes(id))
|
||||
// When current=[1,2,3] and initial=[2,3], added=[1]
|
||||
setupMockServer();
|
||||
describe('Adding environments (drawer)', () => {
|
||||
it('opens the drawer when Add button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
setupMockServer({ availableEnvs: [createEnv(20, 'available-env')] });
|
||||
renderComponent();
|
||||
|
||||
// This test validates the component's internal logic by checking the highlightIds
|
||||
// passed to AssociatedEnvironmentsTable (newly added envs get "Unsaved" badge)
|
||||
renderComponent({
|
||||
associatedEnvironmentIds: [1, 2, 3],
|
||||
initialAssociatedEnvironmentIds: [2, 3],
|
||||
});
|
||||
const addBtn = await screen.findByTestId('add-environments-button');
|
||||
await user.click(addBtn);
|
||||
|
||||
// The component will compute addedIds=[1] internally
|
||||
// We can't directly test internal state, but we verify it renders
|
||||
expect(screen.getByTestId('group-associatedEndpoints')).toBeVisible();
|
||||
expect(await screen.findByText('Add environments')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should identify removed IDs (initial but not current)', () => {
|
||||
// removedIds = initialAssociatedEnvironmentIds.filter(id => !associatedEnvironmentIds.includes(id))
|
||||
// When current=[2,3] and initial=[1,2,3], removed=[1]
|
||||
setupMockServer();
|
||||
it('calls PUT with merged IDs when environments are added from drawer', async () => {
|
||||
const user = userEvent.setup();
|
||||
let requestBody: unknown;
|
||||
|
||||
renderComponent({
|
||||
associatedEnvironmentIds: [2, 3],
|
||||
initialAssociatedEnvironmentIds: [1, 2, 3],
|
||||
setupMockServer({
|
||||
associatedEnvs: [createEnv(10, 'existing-env')],
|
||||
availableEnvs: [createEnv(20, 'new-env')],
|
||||
onPut: (body) => {
|
||||
requestBody = body;
|
||||
},
|
||||
});
|
||||
renderComponent();
|
||||
|
||||
// The component will compute removedIds=[1] internally
|
||||
// and pass it as includeIds to AvailableEnvironmentsTable
|
||||
expect(screen.getByTestId('group-availableEndpoints')).toBeVisible();
|
||||
// Open drawer
|
||||
const addBtn = await screen.findByTestId('add-environments-button');
|
||||
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 { 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 { AssociatedEnvironmentsTable } from './AssociatedEnvironmentsTable';
|
||||
import { AvailableEnvironmentsTable } from './AvailableEnvironmentsTable';
|
||||
import { AddEnvironmentsDrawer } from './AddEnvironmentsDrawer';
|
||||
|
||||
interface Props {
|
||||
/** Group ID when editing an existing group */
|
||||
groupId?: number;
|
||||
/** IDs of currently associated environments */
|
||||
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;
|
||||
groupId: EnvironmentGroupId;
|
||||
/* For unassigned group, don't show the add/remove buttons and hide the checkbox */
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export function AssociatedEnvironmentsSelector({
|
||||
groupId,
|
||||
associatedEnvironmentIds,
|
||||
initialAssociatedEnvironmentIds,
|
||||
onChange,
|
||||
}: Props) {
|
||||
// Track full environment objects for display (populated when clicking rows)
|
||||
const [environmentCache, setEnvironmentCache] = useState<
|
||||
Map<EnvironmentId, EnvironmentTableData>
|
||||
>(new Map());
|
||||
export function AssociatedEnvironmentsSelector({ groupId, readOnly }: Props) {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
// Fetch initially associated environments to populate the cache
|
||||
const initialEnvsQuery = useEnvironmentList(
|
||||
groupId
|
||||
? {
|
||||
groupIds: [groupId],
|
||||
pageLimit: 0,
|
||||
}
|
||||
: {
|
||||
endpointIds: initialAssociatedEnvironmentIds,
|
||||
},
|
||||
{
|
||||
enabled: groupId
|
||||
? groupId !== 1
|
||||
: initialAssociatedEnvironmentIds.length > 0,
|
||||
}
|
||||
);
|
||||
const groupQuery = useGroup(groupId);
|
||||
const environmentsQuery = useEnvironmentList({
|
||||
groupIds: [groupId],
|
||||
pageLimit: 0,
|
||||
});
|
||||
const updateMutation = useUpdateGroupMutation();
|
||||
|
||||
const environmentMap = useMemo(
|
||||
() => buildEnvironmentMap(environmentCache, initialEnvsQuery.environments),
|
||||
[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);
|
||||
const currentEnvironments = environmentsQuery.environments ?? [];
|
||||
const currentIds = currentEnvironments.map((e) => e.Id);
|
||||
|
||||
return (
|
||||
<FormSection title="Associated environments">
|
||||
<div className="small text-muted">
|
||||
You can select which environment should be part of this group by moving
|
||||
them to the associated environments table. Simply click on any
|
||||
environment entry to move it from one table to the other.
|
||||
</div>
|
||||
<>
|
||||
<AssociatedEnvironmentsTable
|
||||
title="Associated environments"
|
||||
environments={currentEnvironments}
|
||||
isLoading={environmentsQuery.isLoading}
|
||||
onRemove={handleRemove}
|
||||
onOpenAddDrawer={() => setDrawerOpen(true)}
|
||||
isRemoving={updateMutation.isLoading}
|
||||
data-cy="group-associatedEndpoints"
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
<div className="flex mt-4 gap-5 items-stretch">
|
||||
<div className="w-1/2 flex flex-col">
|
||||
<AvailableEnvironmentsTable
|
||||
title="Available environments"
|
||||
excludeIds={excludeIdsForAvailableEnvironments}
|
||||
includeIds={removedIds}
|
||||
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>
|
||||
<AddEnvironmentsDrawer
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
excludeGroupIds={[groupId]}
|
||||
onAdd={handleAdd}
|
||||
isLoading={updateMutation.isLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
function handleAddEnvironment(env: EnvironmentTableData) {
|
||||
if (!associatedEnvironmentIds.includes(env.Id)) {
|
||||
setEnvironmentCache((prev) => new Map(prev).set(env.Id, env));
|
||||
onChange([...associatedEnvironmentIds, env.Id]);
|
||||
}
|
||||
function handleRemove(selected: EnvironmentTableData[]) {
|
||||
const selectedIds = new Set(selected.map((e) => e.Id));
|
||||
const remainingIds = currentIds.filter((id) => !selectedIds.has(id));
|
||||
|
||||
updateMutation.mutate({
|
||||
id: groupId,
|
||||
name: groupQuery.data?.Name ?? '',
|
||||
description: groupQuery.data?.Description,
|
||||
tagIds: groupQuery.data?.TagIds,
|
||||
associatedEnvironments: remainingIds,
|
||||
});
|
||||
}
|
||||
|
||||
function handleRemoveEnvironment(env: EnvironmentTableData) {
|
||||
onChange(associatedEnvironmentIds.filter((id) => id !== env.Id));
|
||||
async function handleAdd(newEnvs: EnvironmentTableData[]): Promise<boolean> {
|
||||
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 clsx from 'clsx';
|
||||
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 { useTableStateWithoutStorage } from '@@/datatables/useTableState';
|
||||
import { Datatable, TableRow } from '@@/datatables';
|
||||
import { Badge } from '@@/Badge';
|
||||
import { Widget } from '@@/Widget';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { withControlledSelected } from '@@/datatables/extend-options/withControlledSelected';
|
||||
import { TableRow } from '@@/datatables/TableRow';
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { EnvironmentTableData } from './types';
|
||||
|
||||
@@ -18,64 +20,101 @@ const columnHelper = createColumnHelper<EnvironmentTableData>();
|
||||
interface Props extends AutomationTestingProps {
|
||||
title: string;
|
||||
environments: Array<EnvironmentTableData>;
|
||||
onClickRow?: (env: EnvironmentTableData) => void;
|
||||
highlightIds?: Array<EnvironmentId>;
|
||||
onRemove(selected: EnvironmentTableData[]): void;
|
||||
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({
|
||||
title,
|
||||
environments,
|
||||
onClickRow,
|
||||
highlightIds = [],
|
||||
onRemove,
|
||||
onOpenAddDrawer,
|
||||
isRemoving,
|
||||
isLoading,
|
||||
confirmRemove = true,
|
||||
readOnly = false,
|
||||
'data-cy': dataCy,
|
||||
}: Props) {
|
||||
const tableState = useTableStateWithoutStorage('Name');
|
||||
const columns = useMemo(() => buildColumns(highlightIds), [highlightIds]);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const columns = useMemo(() => buildColumns(), []);
|
||||
|
||||
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'
|
||||
// avoid padding issues with the widget
|
||||
<div className="-mx-[15px]">
|
||||
<Datatable<EnvironmentTableData>
|
||||
disableSelect={readOnly}
|
||||
isLoading={isLoading}
|
||||
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()}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Datatable<EnvironmentTableData>
|
||||
// noWidget to avoid padding issues with TableContainer
|
||||
noWidget
|
||||
title={title}
|
||||
columns={columns}
|
||||
settingsManager={tableState}
|
||||
dataset={environments}
|
||||
renderRow={(row) => (
|
||||
<TableRow<EnvironmentTableData>
|
||||
cells={row.getVisibleCells()}
|
||||
onClick={onClickRow ? () => onClickRow(row.original) : undefined}
|
||||
/>
|
||||
)}
|
||||
disableSelect
|
||||
data-cy={dataCy || 'environment-table'}
|
||||
/>
|
||||
</div>
|
||||
</Widget>
|
||||
extendTableOptions={withControlledSelected(setSelectedIds, selectedIds)}
|
||||
renderTableActions={(selectedItems) =>
|
||||
readOnly ? null : (
|
||||
<>
|
||||
{confirmRemove ? (
|
||||
<DeleteButton
|
||||
disabled={selectedItems.length === 0}
|
||||
isLoading={isRemoving}
|
||||
confirmMessage="Are you sure you want to remove the selected environment(s) from this group?"
|
||||
onConfirmed={() => handleRemove(selectedItems)}
|
||||
data-cy="remove-environments-button"
|
||||
type="button"
|
||||
/>
|
||||
) : (
|
||||
<DeleteButton
|
||||
disabled={selectedItems.length === 0}
|
||||
onClick={() => {
|
||||
handleRemove(selectedItems);
|
||||
}}
|
||||
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 [
|
||||
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.includes(row.original.Id) && (
|
||||
<Badge type="muted" data-cy="unsaved-badge">
|
||||
Unsaved
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
cell: ({ getValue }) => (
|
||||
<span title={getValue()}>{truncate(getValue(), { length: 64 })}</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: '',
|
||||
description: '',
|
||||
tagIds: [],
|
||||
associatedEnvironments: [],
|
||||
},
|
||||
onSubmit = vi.fn(),
|
||||
submitLabel = 'Create',
|
||||
@@ -40,7 +39,7 @@ function renderGroupForm({
|
||||
{ ID: 2, Name: 'staging' },
|
||||
])
|
||||
),
|
||||
// Mock environments list for AssociatedEnvironmentsSelector
|
||||
// Mock environments list for AssociatedEnvironmentsSelector and InlineAvailableEnvironmentsTable
|
||||
http.get('/api/endpoints', () =>
|
||||
HttpResponse.json([], {
|
||||
headers: {
|
||||
@@ -48,6 +47,16 @@ function renderGroupForm({
|
||||
'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();
|
||||
});
|
||||
|
||||
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 });
|
||||
|
||||
// Wait for form to render
|
||||
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(
|
||||
await screen.findByRole('heading', { name: /Associated environments/i })
|
||||
).toBeVisible();
|
||||
screen.queryByRole('heading', { name: /Associated environments/i })
|
||||
).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 });
|
||||
|
||||
// Wait for form to render
|
||||
await screen.findByLabelText(/Name/i);
|
||||
|
||||
// Check for section title using findByRole
|
||||
// In edit mode, environments are managed by AssociatedEnvironmentsSelector component (rendered separately)
|
||||
expect(
|
||||
await screen.findByRole('heading', {
|
||||
name: /Unassociated environments/i,
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
// Should NOT show "Associated environments" section (exact match to exclude "Unassociated")
|
||||
const associatedElements = screen.queryAllByText(
|
||||
/^Associated environments$/i
|
||||
);
|
||||
expect(associatedElements).toHaveLength(0);
|
||||
screen.queryByRole('heading', { name: /Associated environments/i })
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('add-environments-button')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
// Wait for form to render
|
||||
await screen.findByLabelText(/Name/i);
|
||||
|
||||
// Check for section title using findByRole
|
||||
// FormModeEnvironmentsSelector renders AssociatedEnvironmentsTable with an Add button
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: /Associated environments/i })
|
||||
).toBeVisible();
|
||||
await screen.findByTestId('add-environments-button')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,7 +165,6 @@ describe('GroupForm', () => {
|
||||
name: 'existing-group',
|
||||
description: '',
|
||||
tagIds: [],
|
||||
associatedEnvironments: [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -169,7 +172,6 @@ describe('GroupForm', () => {
|
||||
name: /Create/i,
|
||||
});
|
||||
|
||||
// Form is valid but not dirty, so should be disabled
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
@@ -235,7 +237,6 @@ describe('GroupForm', () => {
|
||||
name: 'test-group',
|
||||
description: 'Test description',
|
||||
tagIds: [],
|
||||
associatedEnvironments: [],
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
@@ -244,7 +245,6 @@ describe('GroupForm', () => {
|
||||
|
||||
it('should show loading state during submission', async () => {
|
||||
const user = userEvent.setup();
|
||||
// Create a promise that we can control
|
||||
let resolveSubmit: () => void;
|
||||
const onSubmit = vi.fn().mockImplementation(
|
||||
() =>
|
||||
@@ -269,14 +269,12 @@ describe('GroupForm', () => {
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
// Should show loading state
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: /Creating.../i })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
// Resolve the submission
|
||||
resolveSubmit!();
|
||||
});
|
||||
});
|
||||
@@ -306,7 +304,6 @@ describe('GroupForm', () => {
|
||||
name: 'pre-filled-name',
|
||||
description: 'pre-filled-description',
|
||||
tagIds: [],
|
||||
associatedEnvironments: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,26 +9,28 @@ import { object, string, array, number } from 'yup';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { TagId } from '@/portainer/tags/types';
|
||||
import {
|
||||
EnvironmentId,
|
||||
EnvironmentGroupId,
|
||||
} from '@/react/portainer/environments/types';
|
||||
import { useIsPureAdmin } from '@/react/hooks/useUser';
|
||||
import { useCanExit } from '@/react/hooks/useCanExit';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { FormSection } from '@@/form-components/FormSection';
|
||||
import { TagSelector } from '@@/TagSelector';
|
||||
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 { AssociatedEnvironmentsSelector } from './AssociatedEnvironmentsSelector/AssociatedEnvironmentsSelector';
|
||||
import { AvailableEnvironmentsTable } from './AssociatedEnvironmentsSelector/AvailableEnvironmentsTable';
|
||||
import { FormModeEnvironmentsSelector } from './AssociatedEnvironmentsSelector/FormModeEnvironmentsSelector';
|
||||
|
||||
export interface GroupFormValues {
|
||||
name: string;
|
||||
description: string;
|
||||
tagIds: Array<TagId>;
|
||||
associatedEnvironments: Array<EnvironmentId>;
|
||||
/** Used in create mode only — undefined in edit mode */
|
||||
associatedEnvironments?: Array<EnvironmentId>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -48,7 +50,6 @@ const validationSchema = object({
|
||||
name: string().required('Name is required'),
|
||||
description: string(),
|
||||
tagIds: array(number()),
|
||||
associatedEnvironments: array(),
|
||||
});
|
||||
|
||||
export function GroupForm({
|
||||
@@ -71,7 +72,6 @@ export function GroupForm({
|
||||
enableReinitialize
|
||||
>
|
||||
<InnerForm
|
||||
initialValues={initialValues}
|
||||
submitLabel={submitLabel}
|
||||
submitLoadingLabel={submitLoadingLabel}
|
||||
groupId={groupId}
|
||||
@@ -81,20 +81,17 @@ export function GroupForm({
|
||||
}
|
||||
|
||||
interface InnerFormProps {
|
||||
initialValues: GroupFormValues;
|
||||
submitLabel: string;
|
||||
submitLoadingLabel: string;
|
||||
groupId?: EnvironmentGroupId;
|
||||
}
|
||||
|
||||
function InnerForm({
|
||||
initialValues,
|
||||
submitLabel,
|
||||
submitLoadingLabel,
|
||||
groupId,
|
||||
}: InnerFormProps) {
|
||||
const isPureAdmin = useIsPureAdmin();
|
||||
const isUnassignedGroup = groupId === 1;
|
||||
const {
|
||||
values,
|
||||
errors,
|
||||
@@ -104,6 +101,7 @@ function InnerForm({
|
||||
dirty,
|
||||
isSubmitting,
|
||||
} = useFormikContext<GroupFormValues>();
|
||||
const isCreateMode = !groupId;
|
||||
|
||||
return (
|
||||
<Form className="form-horizontal">
|
||||
@@ -140,31 +138,25 @@ function InnerForm({
|
||||
allowCreate={isPureAdmin}
|
||||
/>
|
||||
|
||||
{isUnassignedGroup ? (
|
||||
<FormSection title="Unassociated environments">
|
||||
<AvailableEnvironmentsTable
|
||||
title="Unassociated environments"
|
||||
excludeIds={[]}
|
||||
data-cy="group-unassociatedEndpoints"
|
||||
/>
|
||||
</FormSection>
|
||||
) : (
|
||||
<AssociatedEnvironmentsSelector
|
||||
groupId={groupId}
|
||||
associatedEnvironmentIds={values.associatedEnvironments}
|
||||
initialAssociatedEnvironmentIds={initialValues.associatedEnvironments}
|
||||
{isCreateMode && (
|
||||
// Same UI as edit mode, but updates form values instead of the API
|
||||
<FormModeEnvironmentsSelector
|
||||
selectedIds={values.associatedEnvironments ?? []}
|
||||
onChange={(ids) => setFieldValue('associatedEnvironments', ids)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormActions
|
||||
submitLabel={submitLabel}
|
||||
loadingText={submitLoadingLabel}
|
||||
isLoading={isSubmitting}
|
||||
isValid={isValid && !isSubmitting && dirty}
|
||||
errors={errors}
|
||||
data-cy="group-submit-button"
|
||||
/>
|
||||
<StickyFooter className="justify-end gap-4">
|
||||
<LoadingButton
|
||||
size="medium"
|
||||
loadingText={submitLoadingLabel}
|
||||
isLoading={isSubmitting}
|
||||
disabled={!isValid || isSubmitting || !dirty}
|
||||
data-cy="group-submit-button"
|
||||
>
|
||||
{submitLabel}
|
||||
</LoadingButton>
|
||||
</StickyFooter>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
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 { notifySuccess } from '@/portainer/services/notifications';
|
||||
|
||||
import { EnvironmentGroupId, EnvironmentId } from '../../types';
|
||||
import { EnvironmentGroup } from '../types';
|
||||
@@ -44,10 +45,11 @@ export function useUpdateGroupMutation() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: updateGroup,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(queryKeys.base());
|
||||
queryClient.invalidateQueries(environmentQueryKeys.base());
|
||||
notifySuccess('Success', 'Group successfully updated');
|
||||
},
|
||||
...withGlobalError('Failed to update group'),
|
||||
...withInvalidate(queryClient, [
|
||||
queryKeys.base(),
|
||||
environmentQueryKeys.base(),
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface BaseEnvironmentsQueryParams {
|
||||
tagIds?: TagId[];
|
||||
endpointIds?: EnvironmentId[];
|
||||
excludeIds?: EnvironmentId[];
|
||||
excludeGroupIds?: EnvironmentGroupId[];
|
||||
tagsPartialMatch?: boolean;
|
||||
groupIds?: EnvironmentGroupId[];
|
||||
status?: EnvironmentStatus[];
|
||||
|
||||
@@ -14,6 +14,10 @@ import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
|
||||
import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector';
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
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 { EdgeAgentTab } from '../shared/EdgeAgentTab';
|
||||
@@ -27,50 +31,78 @@ interface Props {
|
||||
isDockerStandalone?: boolean;
|
||||
}
|
||||
|
||||
const options: BoxSelectorOption<
|
||||
'agent' | 'api' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync'
|
||||
>[] = _.compact([
|
||||
type CreationType =
|
||||
| 'agent'
|
||||
| 'api'
|
||||
| 'socket'
|
||||
| 'edgeAgentStandard'
|
||||
| 'edgeAgentAsync';
|
||||
|
||||
const primaryOptions: BoxSelectorOption<CreationType>[] = _.compact([
|
||||
{
|
||||
id: 'edgeAgentStandard',
|
||||
icon: <BadgeIcon icon={EdgeAgentStandardIcon} size="3xl" />,
|
||||
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',
|
||||
},
|
||||
isBE && {
|
||||
id: 'edgeAgentAsync',
|
||||
icon: <BadgeIcon icon={EdgeAgentAsyncIcon} size="3xl" />,
|
||||
label: 'Edge Agent Async',
|
||||
description: '',
|
||||
value: 'edgeAgentAsync',
|
||||
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' as CreationType,
|
||||
},
|
||||
]);
|
||||
|
||||
const legacyOptions: BoxSelectorOption<CreationType>[] = [
|
||||
{
|
||||
id: 'agent',
|
||||
icon: <BadgeIcon icon={Zap} size="3xl" />,
|
||||
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',
|
||||
},
|
||||
{
|
||||
id: 'api',
|
||||
icon: <BadgeIcon icon={Network} size="3xl" />,
|
||||
label: 'API',
|
||||
description: '',
|
||||
description: 'Connect to the environment directly via the Docker API.',
|
||||
value: 'api',
|
||||
},
|
||||
{
|
||||
id: 'socket',
|
||||
icon: <BadgeIcon icon={Plug2} size="3xl" />,
|
||||
label: 'Socket',
|
||||
description: '',
|
||||
description: 'Connect to the environment directly via the Docker socket.',
|
||||
value: 'socket',
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
const containerEngine = ContainerEngine.Docker;
|
||||
|
||||
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);
|
||||
|
||||
@@ -89,23 +121,43 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
|
||||
)}
|
||||
<BoxSelector
|
||||
onChange={(v) => setCreationType(v)}
|
||||
options={options}
|
||||
options={primaryOptions}
|
||||
value={creationType}
|
||||
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'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}
|
||||
</div>
|
||||
);
|
||||
|
||||
function getTab(
|
||||
creationType:
|
||||
| 'agent'
|
||||
| 'api'
|
||||
| 'socket'
|
||||
| 'edgeAgentStandard'
|
||||
| 'edgeAgentAsync'
|
||||
) {
|
||||
function getTab(creationType: CreationType) {
|
||||
switch (creationType) {
|
||||
case 'agent':
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { render, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||
import { server, http } from '@/setup-tests/server';
|
||||
|
||||
import { WizardKubernetes } from './WizardKubernetes';
|
||||
@@ -37,9 +38,10 @@ function renderComponent() {
|
||||
)
|
||||
);
|
||||
|
||||
const Wrapped = withTestQueryProvider(() => (
|
||||
const WithRouter = withTestRouter(() => (
|
||||
<WizardKubernetes onCreate={() => {}} />
|
||||
));
|
||||
const Wrapped = withTestQueryProvider(WithRouter);
|
||||
return render(<Wrapped />);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
|
||||
import { BoxSelectorOption } from '@@/BoxSelector/types';
|
||||
import { BoxSelector } from '@@/BoxSelector';
|
||||
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 { EdgeAgentTab } from '../shared/EdgeAgentTab';
|
||||
@@ -32,21 +36,26 @@ type CreationType =
|
||||
| 'agent'
|
||||
| 'kubeconfig';
|
||||
|
||||
const options: BoxSelectorOption<CreationType>[] = _.compact([
|
||||
{
|
||||
id: 'agent_endpoint',
|
||||
icon: Zap,
|
||||
iconType: 'badge',
|
||||
label: 'Agent',
|
||||
value: 'agent',
|
||||
description: '',
|
||||
},
|
||||
const primaryOptions: BoxSelectorOption<CreationType>[] = _.compact([
|
||||
{
|
||||
id: 'edgeAgentStandard',
|
||||
icon: EdgeAgentStandardIcon,
|
||||
iconType: 'badge',
|
||||
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',
|
||||
},
|
||||
isBE && {
|
||||
@@ -54,22 +63,40 @@ const options: BoxSelectorOption<CreationType>[] = _.compact([
|
||||
icon: EdgeAgentAsyncIcon,
|
||||
iconType: 'badge',
|
||||
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',
|
||||
},
|
||||
]);
|
||||
|
||||
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',
|
||||
icon: UploadCloud,
|
||||
iconType: 'badge',
|
||||
label: 'Import',
|
||||
value: 'kubeconfig',
|
||||
description: 'Import an existing Kubernetes config',
|
||||
description: 'Import an existing Kubernetes config.',
|
||||
feature: FeatureId.K8S_CREATE_FROM_KUBECONFIG,
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
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);
|
||||
|
||||
@@ -77,11 +104,38 @@ export function WizardKubernetes({ onCreate }: Props) {
|
||||
<div className="form-horizontal">
|
||||
<BoxSelector
|
||||
onChange={(v) => setCreationType(v)}
|
||||
options={options}
|
||||
options={primaryOptions}
|
||||
value={creationType}
|
||||
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'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}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,10 @@ import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
|
||||
import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector';
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
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 { EdgeAgentTab } from '../shared/EdgeAgentTab';
|
||||
@@ -25,43 +29,66 @@ interface Props {
|
||||
onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
|
||||
}
|
||||
|
||||
const options: BoxSelectorOption<
|
||||
'agent' | 'api' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync'
|
||||
>[] = _.compact([
|
||||
type CreationType = 'agent' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync';
|
||||
|
||||
const primaryOptions: BoxSelectorOption<CreationType>[] = _.compact([
|
||||
{
|
||||
id: 'edgeAgentStandard',
|
||||
icon: <BadgeIcon icon={EdgeAgentStandardIcon} size="3xl" />,
|
||||
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',
|
||||
},
|
||||
isBE && {
|
||||
id: 'edgeAgentAsync',
|
||||
icon: <BadgeIcon icon={EdgeAgentAsyncIcon} size="3xl" />,
|
||||
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',
|
||||
},
|
||||
]);
|
||||
|
||||
const legacyOptions: BoxSelectorOption<CreationType>[] = [
|
||||
{
|
||||
id: 'agent',
|
||||
icon: <BadgeIcon icon={Zap} size="3xl" />,
|
||||
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',
|
||||
},
|
||||
{
|
||||
id: 'socket',
|
||||
icon: <BadgeIcon icon={Plug2} size="3xl" />,
|
||||
label: 'Socket',
|
||||
description: '',
|
||||
description: 'Connect to the environment directly via the Docker socket.',
|
||||
value: 'socket',
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
const containerEngine = ContainerEngine.Podman;
|
||||
|
||||
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);
|
||||
|
||||
@@ -69,10 +96,38 @@ export function WizardPodman({ onCreate }: Props) {
|
||||
<div className="form-horizontal">
|
||||
<BoxSelector
|
||||
onChange={(v) => setCreationType(v)}
|
||||
options={options}
|
||||
options={primaryOptions}
|
||||
value={creationType}
|
||||
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'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}>
|
||||
Currently, Portainer only supports <b>Podman 5</b> running in rootful
|
||||
(privileged) mode on <b>CentOS 9</b> Linux environments. Rootless mode
|
||||
@@ -82,14 +137,7 @@ export function WizardPodman({ onCreate }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
function getTab(
|
||||
creationType:
|
||||
| 'agent'
|
||||
| 'api'
|
||||
| 'socket'
|
||||
| 'edgeAgentStandard'
|
||||
| 'edgeAgentAsync'
|
||||
) {
|
||||
function getTab(creationType: CreationType) {
|
||||
switch (creationType) {
|
||||
case 'agent':
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"docker": "v29.2.1"
|
||||
"docker": "v29.3.0"
|
||||
}
|
||||
|
||||
135
go.mod
135
go.mod
@@ -1,6 +1,6 @@
|
||||
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.
|
||||
|
||||
@@ -16,7 +16,7 @@ require (
|
||||
github.com/aws/smithy-go v1.20.3
|
||||
github.com/cbroglie/mustache v1.4.0
|
||||
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/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5
|
||||
github.com/docker/cli v28.5.1+incompatible
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/docker/docker v28.5.1+incompatible
|
||||
github.com/fvbommel/sortorder v1.1.0
|
||||
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/gofrs/uuid v4.2.0+incompatible
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
@@ -54,20 +54,20 @@ require (
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
go.podman.io/image/v5 v5.37.0
|
||||
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/mod v0.29.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/mod v0.31.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sync v0.19.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
helm.sh/helm/v3 v3.18.5
|
||||
k8s.io/api v0.33.3
|
||||
k8s.io/apimachinery v0.33.4
|
||||
k8s.io/cli-runtime v0.33.3
|
||||
k8s.io/client-go v0.33.3
|
||||
k8s.io/kubectl v0.33.3
|
||||
k8s.io/kubelet v0.33.2
|
||||
k8s.io/metrics v0.33.3
|
||||
helm.sh/helm/v3 v3.20.0
|
||||
k8s.io/api v0.35.0
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/cli-runtime v0.35.0
|
||||
k8s.io/client-go v0.35.0
|
||||
k8s.io/kubectl v0.35.0
|
||||
k8s.io/kubelet v0.35.0
|
||||
k8s.io/metrics v0.35.0
|
||||
oras.land/oras-go/v2 v2.6.0
|
||||
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/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // 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/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // 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/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // 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/chai2010/gettext-go v1.0.2 // 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/containerd/api v1.9.0 // 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/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // 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/distribution/reference v0.6.0 // 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-units v0.5.0 // 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/evanphx/json-patch v5.9.11+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // 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/fsnotify/fsevents v0.2.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-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/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-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // 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/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // 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/gosuri/uitable v0.0.4 // 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/go-cleanhttp v0.5.2 // 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/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // 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-isatty v0.0.20 // 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/term v0.5.2 // 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/morikuni/aec v1.0.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/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // 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/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // 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/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // 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/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // 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/stretchr/objx v0.5.2 // 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/x448/float16 v0.8.4 // 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/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/zclconf/go-cty v1.17.0 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20241123155728-2916694fa469 // 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/net/http/httptrace/otelhttptrace v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.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/otlpmetrichttp 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/otlptracehttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.podman.io/storage v1.60.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.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/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.33.3 // indirect
|
||||
k8s.io/apiserver v0.33.3 // indirect
|
||||
k8s.io/component-base v0.33.3 // indirect
|
||||
k8s.io/component-helpers v0.33.3 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.35.0 // indirect
|
||||
k8s.io/apiserver v0.35.0 // indirect
|
||||
k8s.io/component-base v0.35.0 // indirect
|
||||
k8s.io/component-helpers v0.35.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.19.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.20.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.0 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/yaml v1.5.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
tags.cncf.io/container-device-interface v1.0.1 // indirect
|
||||
)
|
||||
|
||||
300
go.sum
300
go.sum
@@ -1,4 +1,6 @@
|
||||
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/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
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/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
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.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
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/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
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/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
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.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
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/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
|
||||
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/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
|
||||
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.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
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/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
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/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
|
||||
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.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
|
||||
github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE=
|
||||
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/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI=
|
||||
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.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
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.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
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.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
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/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
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/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
|
||||
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.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
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/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
|
||||
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
|
||||
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
|
||||
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/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
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/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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/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/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
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/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
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/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.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
||||
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/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.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
|
||||
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/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
|
||||
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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
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.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
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/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/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
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.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
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/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
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.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY=
|
||||
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.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.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.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/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/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
|
||||
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 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
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/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
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/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -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.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||
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.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
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.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
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/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/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/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.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
|
||||
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.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
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.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.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
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/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=
|
||||
@@ -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/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/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
|
||||
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
|
||||
github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
|
||||
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.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
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 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.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.0.0-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-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.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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
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-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.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/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/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/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
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.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
|
||||
github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
|
||||
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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
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/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
|
||||
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.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
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/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
|
||||
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/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
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.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
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/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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/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/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/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
||||
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.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
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.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/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/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/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/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
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/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
|
||||
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/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
|
||||
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.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
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/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
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/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
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/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
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.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
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.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
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/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
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.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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.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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
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-20181108010431-42b317875d0f/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.7.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.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
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-20180909124046-d0be0721c37e/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.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
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/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=
|
||||
@@ -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.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
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.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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.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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
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/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
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-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.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
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/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
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.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/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/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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
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=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
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.18.5/go.mod h1:L/dXDR2r539oPlFP1PJqKAC1CUgqHJDLkxKpDGrWnyg=
|
||||
k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8=
|
||||
k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE=
|
||||
k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs=
|
||||
k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8=
|
||||
k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s=
|
||||
k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4=
|
||||
k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E=
|
||||
k8s.io/cli-runtime v0.33.3 h1:Dgy4vPjNIu8LMJBSvs8W0LcdV0PX/8aGG1DA1W8lklA=
|
||||
k8s.io/cli-runtime v0.33.3/go.mod h1:yklhLklD4vLS8HNGgC9wGiuHWze4g7x6XQZ+8edsKEo=
|
||||
k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA=
|
||||
k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg=
|
||||
k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA=
|
||||
k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4=
|
||||
k8s.io/component-helpers v0.33.3 h1:fjWVORSQfI0WKzPeIFSju/gMD9sybwXBJ7oPbqQu6eM=
|
||||
k8s.io/component-helpers v0.33.3/go.mod h1:7iwv+Y9Guw6X4RrnNQOyQlXcvJrVjPveHVqUA5dm31c=
|
||||
helm.sh/helm/v3 v3.20.0 h1:2M+0qQwnbI1a2CxN7dbmfsWHg/MloeaFMnZCY56as50=
|
||||
helm.sh/helm/v3 v3.20.0/go.mod h1:rTavWa0lagZOxGfdhu4vgk1OjH2UYCnrDKE2PVC4N0o=
|
||||
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
||||
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
||||
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
|
||||
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
|
||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4=
|
||||
k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds=
|
||||
k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE=
|
||||
k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY=
|
||||
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
||||
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
||||
k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
|
||||
k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
|
||||
k8s.io/component-helpers v0.35.0 h1:wcXv7HJRksgVjM4VlXJ1CNFBpyDHruRI99RrBtrJceA=
|
||||
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/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||
k8s.io/kubectl v0.33.3 h1:r/phHvH1iU7gO/l7tTjQk2K01ER7/OAJi8uFHHyWSac=
|
||||
k8s.io/kubectl v0.33.3/go.mod h1:euj2bG56L6kUGOE/ckZbCoudPwuj4Kud7BR0GzyNiT0=
|
||||
k8s.io/kubelet v0.33.2 h1:wxEau5/563oJb3j3KfrCKlNWWx35YlSgDLOYUBCQ0pg=
|
||||
k8s.io/kubelet v0.33.2/go.mod h1:way8VCDTUMiX1HTOvJv7M3xS/xNysJI6qh7TOqMe5KM=
|
||||
k8s.io/metrics v0.33.3 h1:9CcqBz15JZfISqwca33gdHS8I6XfsK1vA8WUdEnG70g=
|
||||
k8s.io/metrics v0.33.3/go.mod h1:Aw+cdg4AYHw0HvUY+lCyq40FOO84awrqvJRTw0cmXDs=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc=
|
||||
k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo=
|
||||
k8s.io/kubelet v0.35.0 h1:8cgJHCBCKLYuuQ7/Pxb/qWbJfX1LXIw7790ce9xHq7c=
|
||||
k8s.io/kubelet v0.35.0/go.mod h1:ciRzAXn7C4z5iB7FhG1L2CGPPXLTVCABDlbXt/Zz8YA=
|
||||
k8s.io/metrics v0.35.0 h1:xVFoqtAGm2dMNJAcB5TFZJPCen0uEqqNt52wW7ABbX8=
|
||||
k8s.io/metrics v0.35.0/go.mod h1:g2Up4dcBygZi2kQSEQVDByFs+VUwepJMzzQLJJLpq4M=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
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/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-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=
|
||||
sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o=
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
|
||||
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ=
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
|
||||
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
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=
|
||||
tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc=
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "@portainer/ce",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "2.39.0",
|
||||
"version": "2.39.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
|
||||
Reference in New Issue
Block a user