Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions internal/cmd/root/products/konnect/adopt/portal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ func (p *portalAPIStub) DeletePortal(context.Context, string, bool) (*kkOps.Dele
return nil, nil
}

func (p *portalAPIStub) ClearPortalDefaultAuthStrategyID(context.Context, string) error {
p.t.Fatalf("unexpected ClearPortalDefaultAuthStrategyID call")
return nil
}

func TestAdoptPortalAssignsNamespaceLabel(t *testing.T) {
helper := new(cmd.MockHelper)
helper.EXPECT().GetContext().Return(context.Background())
Expand Down
4 changes: 3 additions & 1 deletion internal/cmd/root/products/konnect/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ func KonnectSDKFactory(cfg config.Hook, logger *slog.Logger) (helpers.SDKAPI, er
}

return &helpers.KonnectSDK{
SDK: sdk,
SDK: sdk,
BaseURL: baseURL,
BearerToken: token,
}, nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import (
"strings"
"time"

"charm.land/bubbles/v2/table"
kkComps "github.com/Kong/sdk-konnect-go/models/components"
kkOps "github.com/Kong/sdk-konnect-go/models/operations"
"charm.land/bubbles/v2/table"
"github.com/kong/kongctl/internal/cmd"
cmdCommon "github.com/kong/kongctl/internal/cmd/common"
"github.com/kong/kongctl/internal/cmd/output/tableview"
Expand Down Expand Up @@ -45,15 +45,15 @@ type clusterPolicySummaryRecord struct {
// clusterPolicyWithConfig is a wrapper that includes the full config from raw API response.
// The SDK's EventGatewayPolicyConfig struct is empty, so we use map[string]any to capture actual config.
type clusterPolicyWithConfig struct {
Type string `json:"type" yaml:"type"`
Name *string `json:"name,omitempty" yaml:"name,omitempty"`
Description *string `json:"description,omitempty" yaml:"description,omitempty"`
Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
ID string `json:"id" yaml:"id"`
Config map[string]any `json:"config" yaml:"config"`
CreatedAt time.Time `json:"created_at" yaml:"created_at"`
UpdatedAt time.Time `json:"updated_at" yaml:"updated_at"`
Type string `json:"type" yaml:"type"`
Name *string `json:"name,omitempty" yaml:"name,omitempty"`
Description *string `json:"description,omitempty" yaml:"description,omitempty"`
Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
ID string `json:"id" yaml:"id"`
Config map[string]any `json:"config" yaml:"config"`
CreatedAt time.Time `json:"created_at" yaml:"created_at"`
UpdatedAt time.Time `json:"updated_at" yaml:"updated_at"`
ParentPolicyID *string `json:"parent_policy_id,omitempty" yaml:"parent_policy_id,omitempty"`
}

Expand All @@ -62,8 +62,10 @@ var (

clusterPoliciesShort = i18n.T("root.products.konnect.eventgateway.clusterPoliciesShort",
"Manage cluster policies for an Event Gateway Virtual Cluster")
clusterPoliciesLong = normalizers.LongDesc(i18n.T("root.products.konnect.eventgateway.clusterPoliciesLong",
`Use the cluster-policies command to list or retrieve cluster policies for a specific Event Gateway Virtual Cluster.`)) //nolint:lll
clusterPoliciesLong = normalizers.LongDesc(i18n.T(
"root.products.konnect.eventgateway.clusterPoliciesLong",
`Use the cluster-policies command to list or retrieve cluster policies for a specific Event Gateway Virtual Cluster.`,
)) //nolint:lll
clusterPoliciesExample = normalizers.Examples(
i18n.T("root.products.konnect.eventgateway.clusterPoliciesExamples",
fmt.Sprintf(`
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/root/verbs/scaffold/scaffold.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
)

const (
Verb = verbs.Scaffold
Verb = verbs.Scaffold
outputFlagUnsupportedMsg = "flags -o/--" + cmdcommon.OutputFlagName + " are not supported for the scaffold command"
)

Expand Down
6 changes: 4 additions & 2 deletions internal/declarative/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1861,7 +1861,8 @@ func (e *Executor) createResource(ctx context.Context, change *planner.PlannedCh
change.References["event_gateway_id"] = gatewayRef
}
// Resolve event gateway virtual cluster reference if needed
if virtualClusterRef, ok := change.References["event_gateway_virtual_cluster_id"]; ok && virtualClusterRef.ID == "" {
if virtualClusterRef, ok := change.References["event_gateway_virtual_cluster_id"]; ok &&
virtualClusterRef.ID == "" {
gatewayID := change.References["event_gateway_id"].ID
virtualClusterID, err := e.resolveEventGatewayVirtualClusterRef(ctx, gatewayID, virtualClusterRef)
if err != nil {
Expand Down Expand Up @@ -2172,7 +2173,8 @@ func (e *Executor) updateResource(ctx context.Context, change *planner.PlannedCh
change.References["event_gateway_id"] = gatewayRef
}
// Resolve event gateway virtual cluster reference if needed
if virtualClusterRef, ok := change.References["event_gateway_virtual_cluster_id"]; ok && virtualClusterRef.ID == "" {
if virtualClusterRef, ok := change.References["event_gateway_virtual_cluster_id"]; ok &&
virtualClusterRef.ID == "" {
gatewayID := change.References["event_gateway_id"].ID
virtualClusterID, err := e.resolveEventGatewayVirtualClusterRef(ctx, gatewayID, virtualClusterRef)
if err != nil {
Expand Down
17 changes: 16 additions & 1 deletion internal/declarative/executor/portal_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,12 @@ func (e *Executor) updatePortal(ctx context.Context, change planner.PlannedChang
}
case "default_application_auth_strategy_id":
if authID, ok := value.(string); ok {
updatePortal.DefaultApplicationAuthStrategyID = &authID
if authID == planner.ClearDefaultAuthStrategyID {
// Sentinel: unset via explicit null PATCH after the SDK update completes.
// We skip setting the SDK field here; the clear call is made below.
} else {
updatePortal.DefaultApplicationAuthStrategyID = &authID
}
}
case "default_api_visibility":
if visibility, ok := value.(string); ok {
Expand Down Expand Up @@ -203,6 +208,16 @@ func (e *Executor) updatePortal(ctx context.Context, change planner.PlannedChang
return "", err
}

// If the plan requested clearing default_application_auth_strategy_id, send
// an explicit JSON null via a separate raw PATCH (the SDK's UpdatePortal
// struct cannot express explicit null due to omitempty).
if authID, ok := change.Fields["default_application_auth_strategy_id"].(string); ok &&
authID == planner.ClearDefaultAuthStrategyID {
if err := e.client.ClearPortalDefaultAuthStrategyID(ctx, change.ResourceID, change.Namespace); err != nil {
return "", fmt.Errorf("clear portal default auth strategy: %w", err)
}
}

return resp.ID, nil
}

Expand Down
5 changes: 5 additions & 0 deletions internal/declarative/executor/portal_operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ func (m *MockPortalAPI) DeletePortal(
return args.Get(0).(*kkOps.DeletePortalResponse), args.Error(1)
}

func (m *MockPortalAPI) ClearPortalDefaultAuthStrategyID(ctx context.Context, portalID string) error {
args := m.Called(ctx, portalID)
return args.Error(0)
}

func TestExecutor_createPortal(t *testing.T) {
tests := []struct {
name string
Expand Down
19 changes: 19 additions & 0 deletions internal/declarative/planner/portal_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ import (
"github.com/kong/kongctl/internal/declarative/tags"
)

// ClearDefaultAuthStrategyID is the sentinel value written into
// PlannedChange.Fields["default_application_auth_strategy_id"] when the
// desired configuration omits the field but the current portal still has it
// set. The executor recognises this value and sends an explicit JSON null via
// ClearPortalDefaultAuthStrategyID instead of using the SDK's UpdatePortal
// (which cannot express explicit null due to omitempty).
const ClearDefaultAuthStrategyID = "$clear-default-auth-strategy"

// portalPlannerImpl implements planning logic for portal resources
type portalPlannerImpl struct {
*BasePlanner
Expand Down Expand Up @@ -469,6 +477,17 @@ func (p *portalPlannerImpl) shouldUpdatePortal(
}
}
}
} else {
// When the field is absent from the desired configuration but the current
// portal has it set, plan an UPDATE to clear it.
currentAuthID := p.GetString(current.DefaultApplicationAuthStrategyID)
if currentAuthID != "" {
updates["default_application_auth_strategy_id"] = ClearDefaultAuthStrategyID
changedFields["default_application_auth_strategy_id"] = FieldChange{
Old: currentAuthID,
New: nil,
}
}
}

if desired.AuthenticationEnabled != nil {
Expand Down
5 changes: 5 additions & 0 deletions internal/declarative/planner/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ func (m *MockPortalAPI) DeletePortal(
return args.Get(0).(*kkOps.DeletePortalResponse), args.Error(1)
}

func (m *MockPortalAPI) ClearPortalDefaultAuthStrategyID(ctx context.Context, portalID string) error {
args := m.Called(ctx, portalID)
return args.Error(0)
}

// MockAPIAPI is a mock implementation of APIAPI
type MockAPIAPI struct {
mock.Mock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ func init() {
// The SDK represents cluster policies as a union type (EventGatewayClusterPolicyModify)
// with the "acls" variant (EventGatewayACLsPolicy).
type EventGatewayClusterPolicyResource struct {
kkComps.EventGatewayClusterPolicyModify `yaml:",inline" json:",inline"`
Ref string `yaml:"ref" json:"ref"`
kkComps.EventGatewayClusterPolicyModify ` yaml:",inline" json:",inline"`
Ref string `yaml:"ref" json:"ref"`
// Parent Event Gateway Virtual Cluster reference (for root-level definitions)
VirtualCluster string `yaml:"virtual_cluster,omitempty" json:"virtual_cluster,omitempty"`
EventGateway string `yaml:"event_gateway,omitempty" json:"event_gateway,omitempty"`
Expand Down
74 changes: 37 additions & 37 deletions internal/declarative/resources/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ type ExplainBuildContext struct {
}

type ExplainRelation struct {
ParentAlias string `json:"parent_alias" yaml:"parent_alias"`
ParentType string `json:"parent_type" yaml:"parent_type"`
FieldName string `json:"field_name" yaml:"field_name"`
ChildAlias string `json:"child_alias" yaml:"child_alias"`
ParentAlias string `json:"parent_alias" yaml:"parent_alias"`
ParentType string `json:"parent_type" yaml:"parent_type"`
FieldName string `json:"field_name" yaml:"field_name"`
ChildAlias string `json:"child_alias" yaml:"child_alias"`
ParentRootKey string `json:"parent_root_key" yaml:"parent_root_key"`
}

Expand All @@ -66,7 +66,7 @@ type ExplainDoc struct {
ParentRelations []ExplainRelation `json:"parent_relations,omitempty" yaml:"parent_relations,omitempty"`
SupportsKongctl bool `json:"supports_kongctl" yaml:"supports_kongctl"`
Schema *ExplainNode `json:"-" yaml:"-"`
nestedFields map[string]ResourceType
nestedFields map[string]ResourceType
}

type ExplainSubject struct {
Expand Down Expand Up @@ -123,49 +123,49 @@ type ExplainField struct {
}

type JSONSchema struct {
Schema string `json:"$schema,omitempty" yaml:"$schema,omitempty"`
ID string `json:"$id,omitempty" yaml:"$id,omitempty"`
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Type any `json:"type,omitempty" yaml:"type,omitempty"`
Properties map[string]*JSONSchema `json:"properties,omitempty" yaml:"properties,omitempty"`
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
Items *JSONSchema `json:"items,omitempty" yaml:"items,omitempty"`
Additional any `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
OneOf []*JSONSchema `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
Const any `json:"const,omitempty" yaml:"const,omitempty"`
Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"`
Default any `json:"default,omitempty" yaml:"default,omitempty"`
XResource any `json:"x-kongctl-resource,omitempty" yaml:"x-kongctl-resource,omitempty"`
XPath string `json:"x-kongctl-path,omitempty" yaml:"x-kongctl-path,omitempty"`
XRootKey string `json:"x-kongctl-root-key,omitempty" yaml:"x-kongctl-root-key,omitempty"`
XClass string `json:"x-kongctl-resource-class,omitempty" yaml:"x-kongctl-resource-class,omitempty"` //nolint:lll
XRefKind string `json:"x-kongctl-ref-kind,omitempty" yaml:"x-kongctl-ref-kind,omitempty"`
XTag string `json:"x-kongctl-preferred-tag,omitempty" yaml:"x-kongctl-preferred-tag,omitempty"`
XDefault string `json:"x-kongctl-default-from,omitempty" yaml:"x-kongctl-default-from,omitempty"`
XNotes []string `json:"x-kongctl-notes,omitempty" yaml:"x-kongctl-notes,omitempty"`
XSubject *ExplainSchemaSubject `json:"x-kongctl-subject,omitempty" yaml:"x-kongctl-subject,omitempty"`
XPlacement *ExplainSchemaPlacement `json:"x-kongctl-placement,omitempty" yaml:"x-kongctl-placement,omitempty"`
XRoot *bool `json:"x-kongctl-supports-root,omitempty" yaml:"x-kongctl-supports-root,omitempty"`
Schema string `json:"$schema,omitempty" yaml:"$schema,omitempty"`
ID string `json:"$id,omitempty" yaml:"$id,omitempty"`
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Type any `json:"type,omitempty" yaml:"type,omitempty"`
Properties map[string]*JSONSchema `json:"properties,omitempty" yaml:"properties,omitempty"`
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
Items *JSONSchema `json:"items,omitempty" yaml:"items,omitempty"`
Additional any `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
OneOf []*JSONSchema `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
Const any `json:"const,omitempty" yaml:"const,omitempty"`
Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"`
Default any `json:"default,omitempty" yaml:"default,omitempty"`
XResource any `json:"x-kongctl-resource,omitempty" yaml:"x-kongctl-resource,omitempty"`
XPath string `json:"x-kongctl-path,omitempty" yaml:"x-kongctl-path,omitempty"`
XRootKey string `json:"x-kongctl-root-key,omitempty" yaml:"x-kongctl-root-key,omitempty"`
XClass string `json:"x-kongctl-resource-class,omitempty" yaml:"x-kongctl-resource-class,omitempty"` //nolint:lll
XRefKind string `json:"x-kongctl-ref-kind,omitempty" yaml:"x-kongctl-ref-kind,omitempty"`
XTag string `json:"x-kongctl-preferred-tag,omitempty" yaml:"x-kongctl-preferred-tag,omitempty"`
XDefault string `json:"x-kongctl-default-from,omitempty" yaml:"x-kongctl-default-from,omitempty"`
XNotes []string `json:"x-kongctl-notes,omitempty" yaml:"x-kongctl-notes,omitempty"`
XSubject *ExplainSchemaSubject `json:"x-kongctl-subject,omitempty" yaml:"x-kongctl-subject,omitempty"`
XPlacement *ExplainSchemaPlacement `json:"x-kongctl-placement,omitempty" yaml:"x-kongctl-placement,omitempty"`
XRoot *bool `json:"x-kongctl-supports-root,omitempty" yaml:"x-kongctl-supports-root,omitempty"`
XNestedDecl *bool `json:"x-kongctl-supports-nested-declaration,omitempty" yaml:"x-kongctl-supports-nested-declaration,omitempty"` //nolint:lll
}

type ExplainSchemaSubject struct {
Kind string `json:"kind" yaml:"kind"`
Path string `json:"path" yaml:"path"`
Required *bool `json:"required,omitempty" yaml:"required,omitempty"`
Kind string `json:"kind" yaml:"kind"`
Path string `json:"path" yaml:"path"`
Required *bool `json:"required,omitempty" yaml:"required,omitempty"`
Recommended *bool `json:"recommended,omitempty" yaml:"recommended,omitempty"`
}

type ExplainSchemaPlacement struct {
YAMLPath string `json:"yaml_path,omitempty" yaml:"yaml_path,omitempty"`
RootYAMLPath string `json:"root_yaml_path,omitempty" yaml:"root_yaml_path,omitempty"`
NestedYAMLPath string `json:"nested_yaml_path,omitempty" yaml:"nested_yaml_path,omitempty"`
YAMLPath string `json:"yaml_path,omitempty" yaml:"yaml_path,omitempty"`
RootYAMLPath string `json:"root_yaml_path,omitempty" yaml:"root_yaml_path,omitempty"`
NestedYAMLPath string `json:"nested_yaml_path,omitempty" yaml:"nested_yaml_path,omitempty"`
NestedYAMLPaths []string `json:"nested_yaml_paths,omitempty" yaml:"nested_yaml_paths,omitempty"`
}

type ExplainSchemaResource struct {
Name string `json:"name" yaml:"name"`
Name string `json:"name" yaml:"name"`
ResourceClass string `json:"resource_class" yaml:"resource_class"`
}

Expand Down Expand Up @@ -463,7 +463,7 @@ func buildExplainDoc(rt ResourceType) (*ExplainDoc, error) {
ParentRelations: parentRelations,
SupportsKongctl: node.propertyExists("kongctl"),
Schema: node,
nestedFields: nestedFieldMap(childRelations),
nestedFields: nestedFieldMap(childRelations),
}

explainDocCacheMu.Lock()
Expand Down
Loading
Loading