diff --git a/CHANGELOG.md b/CHANGELOG.md index 7703bf947..999001f33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - (Bugfix) Helm Lint Indent fix - (Feature) Add CA Certificates - (Feature) Chart By Tag filter +- (Feature) Add Gateway Config condition ## [1.3.0](https://github.com/arangodb/kube-arangodb/tree/1.3.0) (2025-08-01) - (Feature) (Platform) Storage Debug diff --git a/pkg/apis/deployment/v1/conditions.go b/pkg/apis/deployment/v1/conditions.go index eda4a9c9c..5549a882f 100644 --- a/pkg/apis/deployment/v1/conditions.go +++ b/pkg/apis/deployment/v1/conditions.go @@ -135,6 +135,9 @@ const ( ConditionTypeDBServerWithData ConditionType = "DBServerWithData" // ConditionTypeSyncEnabled Define if DBServer contains any active data leaders ConditionTypeDBServerWithDataLeader ConditionType = "DBServerWithDataLeader" + + // ConditionTypeGatewayConfig contains current config checksum of the Gateway + ConditionTypeGatewayConfig ConditionType = "GatewayConfig" ) // Condition represents one current condition of a deployment or deployment member. diff --git a/pkg/apis/deployment/v2alpha1/conditions.go b/pkg/apis/deployment/v2alpha1/conditions.go index f97d4c75d..f9bdb0288 100644 --- a/pkg/apis/deployment/v2alpha1/conditions.go +++ b/pkg/apis/deployment/v2alpha1/conditions.go @@ -135,6 +135,9 @@ const ( ConditionTypeDBServerWithData ConditionType = "DBServerWithData" // ConditionTypeSyncEnabled Define if DBServer contains any active data leaders ConditionTypeDBServerWithDataLeader ConditionType = "DBServerWithDataLeader" + + // ConditionTypeGatewayConfig contains current config checksum of the Gateway + ConditionTypeGatewayConfig ConditionType = "GatewayConfig" ) // Condition represents one current condition of a deployment or deployment member. diff --git a/pkg/deployment/client/client.go b/pkg/deployment/client/client.go index fd2a08b2f..113007182 100644 --- a/pkg/deployment/client/client.go +++ b/pkg/deployment/client/client.go @@ -57,6 +57,8 @@ type Client interface { DeleteExpiredJobs(ctx context.Context, timeout time.Duration) error Compact(ctx context.Context, request *CompactRequest) error + + Inventory(ctx context.Context) (*Inventory, error) } type client struct { diff --git a/pkg/deployment/client/inventory.go b/pkg/deployment/client/inventory.go new file mode 100644 index 000000000..31ff5ff6e --- /dev/null +++ b/pkg/deployment/client/inventory.go @@ -0,0 +1,60 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package client + +import ( + "context" + goHttp "net/http" + + utilConstants "github.com/arangodb/kube-arangodb/pkg/util/constants" +) + +type Inventory struct { + Configuration InventoryConfiguration `json:"configuration"` +} + +type InventoryConfiguration struct { + Hash string `json:"hash"` +} + +func (c *client) Inventory(ctx context.Context) (*Inventory, error) { + req, err := c.c.NewRequest(goHttp.MethodGet, utilConstants.EnvoyInventoryConfigDestination) + if err != nil { + return nil, err + } + + resp, err := c.c.Do(ctx, req) + if err != nil { + return nil, err + } + + if err := resp.CheckStatus(goHttp.StatusOK); err != nil { + return nil, err + } + + var l Inventory + + if err := resp.ParseBody("", &l); err != nil { + return nil, err + } + + return &l, nil +} diff --git a/pkg/deployment/reconcile/context.go b/pkg/deployment/reconcile/context.go index 0c051b468..bf5578145 100644 --- a/pkg/deployment/reconcile/context.go +++ b/pkg/deployment/reconcile/context.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ type Context interface { reconciler.DeploymentImageManager reconciler.ArangoAgencyGet reconciler.ArangoApplier - reconciler.DeploymentInfoGetter + reconciler.DeploymentGetter reconciler.DeploymentDatabaseClient reconciler.KubernetesEventGenerator diff --git a/pkg/deployment/reconcile/plan_builder_context.go b/pkg/deployment/reconcile/plan_builder_context.go index eb3fdea4b..dae93faa8 100644 --- a/pkg/deployment/reconcile/plan_builder_context.go +++ b/pkg/deployment/reconcile/plan_builder_context.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ import ( type PlanBuilderContext interface { reconciler.DeploymentStatusUpdate - reconciler.DeploymentInfoGetter + reconciler.DeploymentGetter reconciler.DeploymentAgencyMaintenance reconciler.DeploymentPodRenderer reconciler.DeploymentImageManager diff --git a/pkg/deployment/reconcile/plan_builder_gateway.go b/pkg/deployment/reconcile/plan_builder_gateway.go new file mode 100644 index 000000000..f4f3edccf --- /dev/null +++ b/pkg/deployment/reconcile/plan_builder_gateway.go @@ -0,0 +1,72 @@ +// +// DISCLAIMER +// +// Copyright 2025 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package reconcile + +import ( + "context" + "time" + + core "k8s.io/api/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + client "github.com/arangodb/kube-arangodb/pkg/deployment/client" + sharedReconcile "github.com/arangodb/kube-arangodb/pkg/deployment/reconcile/shared" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +func (r *Reconciler) createMemberGatewayConfigConditionPlan(ctx context.Context, _ k8sutil.APIObject, _ api.DeploymentSpec, + status api.DeploymentStatus, planCtx PlanBuilderContext) api.Plan { + var plan api.Plan + + // Check for members in failed state. + for _, m := range status.Members.AsListInGroup(api.ServerGroupGateways) { + inv, err := r.getGatewayInventoryConfig(ctx, planCtx, m.Group, m.Member) + if err != nil { + if c, ok := m.Member.Conditions.Get(api.ConditionTypeGatewayConfig); !ok || c.Status == core.ConditionTrue { + plan = append(plan, sharedReconcile.UpdateMemberConditionActionV2("Config is not present", api.ConditionTypeGatewayConfig, m.Group, m.Member.ID, false, "Config is not present", "Config is not present", "")) + } + + continue + } + + logger.JSON("inv", inv).Info("Inventory Fetched") + + if c, ok := m.Member.Conditions.Get(api.ConditionTypeGatewayConfig); !ok || c.Status == core.ConditionFalse || c.Hash != inv.Configuration.Hash { + plan = append(plan, sharedReconcile.UpdateMemberConditionActionV2("Config Present", api.ConditionTypeGatewayConfig, m.Group, m.Member.ID, true, "Config Present", "Config Present", inv.Configuration.Hash)) + } + } + + return plan +} + +func (r *Reconciler) getGatewayInventoryConfig(ctx context.Context, planCtx PlanBuilderContext, group api.ServerGroup, member api.MemberStatus) (*client.Inventory, error) { + serverClient, err := planCtx.GetServerClient(ctx, group, member.ID) + if err != nil { + return nil, err + } + + internalClient := client.NewClient(serverClient.Connection(), logger) + + lCtx, c := context.WithTimeout(ctx, 500*time.Millisecond) + defer c() + + return internalClient.Inventory(lCtx) +} diff --git a/pkg/deployment/reconcile/plan_builder_high.go b/pkg/deployment/reconcile/plan_builder_high.go index 1341ee775..67ba5b839 100644 --- a/pkg/deployment/reconcile/plan_builder_high.go +++ b/pkg/deployment/reconcile/plan_builder_high.go @@ -55,6 +55,7 @@ func (r *Reconciler) createHighPlan(ctx context.Context, apiObject k8sutil.APIOb ApplyIfEmpty(r.updateMemberUpdateConditionsPlan). ApplyIfEmpty(r.updateMemberRotationConditionsPlan). ApplyIfEmpty(r.createMemberAllowUpgradeConditionPlan). + ApplyIfEmpty(r.createMemberGatewayConfigConditionPlan). ApplyIfEmpty(r.createMemberRecreationConditionsPlan). ApplyIfEmpty(r.createMemberPodSchedulingFailurePlan). ApplyIfEmpty(r.createRotateServerStoragePVCPendingResizeConditionPlan). diff --git a/pkg/deployment/reconcile/plan_builder_test.go b/pkg/deployment/reconcile/plan_builder_test.go index 422f2422a..73538513e 100644 --- a/pkg/deployment/reconcile/plan_builder_test.go +++ b/pkg/deployment/reconcile/plan_builder_test.go @@ -82,6 +82,16 @@ type testContext struct { state member.StateInspector } +func (c *testContext) GetServerClient(ctx context.Context, group api.ServerGroup, id string) (driver.Client, error) { + //TODO implement me + panic("implement me") +} + +func (c *testContext) GetSyncServerClient(ctx context.Context, group api.ServerGroup, id string) (client.API, error) { + //TODO implement me + panic("implement me") +} + func (c *testContext) IsSyncEnabled() bool { return false } diff --git a/pkg/deployment/resources/config_map_gateway.go b/pkg/deployment/resources/config_map_gateway.go index 9ebaa49c7..db35c7836 100644 --- a/pkg/deployment/resources/config_map_gateway.go +++ b/pkg/deployment/resources/config_map_gateway.go @@ -53,37 +53,6 @@ func (r *Resources) ensureGatewayConfig(ctx context.Context, cachedStatus inspec return errors.WithStack(errors.Wrapf(err, "Failed to generate gateway config")) } - _, baseGatewayCfgYamlChecksum, _, err := cfg.RenderYAML() - if err != nil { - return errors.WithStack(errors.Wrapf(err, "Failed to render gateway config")) - } - - cfg.Destinations[utilConstants.EnvoyInventoryConfigDestination] = gateway.ConfigDestination{ - Type: util.NewType(gateway.ConfigDestinationTypeStatic), - Match: util.NewType(gateway.ConfigMatchPath), - AuthExtension: &gateway.ConfigAuthZExtension{ - AuthZExtension: map[string]string{ - pbImplEnvoyAuthV3Shared.AuthConfigAuthRequiredKey: pbImplEnvoyAuthV3Shared.AuthConfigKeywordTrue, - pbImplEnvoyAuthV3Shared.AuthConfigAuthPassModeKey: string(networkingApi.ArangoRouteSpecAuthenticationPassModeRemove), - }, - }, - Static: &gateway.ConfigDestinationStatic[*pbInventoryV1.Inventory]{ - Code: util.NewType[uint32](200), - Response: &pbInventoryV1.Inventory{ - Configuration: &pbInventoryV1.InventoryConfiguration{ - Hash: baseGatewayCfgYamlChecksum, - }, - Arangodb: pbInventoryV1.NewArangoDBConfiguration(r.context.GetSpec(), r.context.GetStatus()), - }, - Marshaller: ugrpc.Marshal[*pbInventoryV1.Inventory], - Options: []util.Mod[protojson.MarshalOptions]{ - func(in *protojson.MarshalOptions) { - in.EmitDefaultValues = true - }, - }, - }, - } - cfg.Destinations[utilConstants.EnvoyIdentityDestination] = gateway.ConfigDestination{ Type: util.NewType(gateway.ConfigDestinationTypeHTTP), Match: util.NewType(gateway.ConfigMatchPath), @@ -138,6 +107,37 @@ func (r *Resources) ensureGatewayConfig(ctx context.Context, cachedStatus inspec }, } + _, baseGatewayCfgYamlChecksum, _, err := cfg.RenderYAML() + if err != nil { + return errors.WithStack(errors.Wrapf(err, "Failed to render gateway config")) + } + + cfg.Destinations[utilConstants.EnvoyInventoryConfigDestination] = gateway.ConfigDestination{ + Type: util.NewType(gateway.ConfigDestinationTypeStatic), + Match: util.NewType(gateway.ConfigMatchPath), + AuthExtension: &gateway.ConfigAuthZExtension{ + AuthZExtension: map[string]string{ + pbImplEnvoyAuthV3Shared.AuthConfigAuthRequiredKey: pbImplEnvoyAuthV3Shared.AuthConfigKeywordTrue, + pbImplEnvoyAuthV3Shared.AuthConfigAuthPassModeKey: string(networkingApi.ArangoRouteSpecAuthenticationPassModeRemove), + }, + }, + Static: &gateway.ConfigDestinationStatic[*pbInventoryV1.Inventory]{ + Code: util.NewType[uint32](200), + Response: &pbInventoryV1.Inventory{ + Configuration: &pbInventoryV1.InventoryConfiguration{ + Hash: baseGatewayCfgYamlChecksum, + }, + Arangodb: pbInventoryV1.NewArangoDBConfiguration(r.context.GetSpec(), r.context.GetStatus()), + }, + Marshaller: ugrpc.Marshal[*pbInventoryV1.Inventory], + Options: []util.Mod[protojson.MarshalOptions]{ + func(in *protojson.MarshalOptions) { + in.EmitDefaultValues = true + }, + }, + }, + } + gatewayCfgYaml, _, _, err := cfg.RenderYAML() if err != nil { return errors.WithStack(errors.Wrapf(err, "Failed to render gateway config")) diff --git a/pkg/deployment/resources/config_maps.go b/pkg/deployment/resources/config_maps.go index 0fbf34d89..0fef820cc 100644 --- a/pkg/deployment/resources/config_maps.go +++ b/pkg/deployment/resources/config_maps.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// Copyright 2024-2025 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -51,5 +51,6 @@ func (r *Resources) EnsureConfigMaps(ctx context.Context, cachedStatus inspector return errors.Section(err, "Member ConfigMap") } } + return reconcileRequired.Reconcile(ctx) } diff --git a/pkg/util/grpc/grpc.go b/pkg/util/grpc/grpc.go index 510e61399..876c6470e 100644 --- a/pkg/util/grpc/grpc.go +++ b/pkg/util/grpc/grpc.go @@ -156,7 +156,11 @@ type GRPC[T proto.Message] struct { Object T } -func (g *GRPC[T]) UnmarshalJSON(data []byte, opts ...util.Mod[protojson.UnmarshalOptions]) error { +func (g *GRPC[T]) UnmarshalJSON(data []byte) error { + return g.UnmarshalJSONOpts(data) +} + +func (g *GRPC[T]) UnmarshalJSONOpts(data []byte, opts ...util.Mod[protojson.UnmarshalOptions]) error { o, err := Unmarshal[T](data, opts...) if err != nil { return err @@ -166,6 +170,10 @@ func (g *GRPC[T]) UnmarshalJSON(data []byte, opts ...util.Mod[protojson.Unmarsha return nil } -func (g GRPC[T]) MarshalJSON(opts ...util.Mod[protojson.MarshalOptions]) ([]byte, error) { +func (g GRPC[T]) MarshalJSON() ([]byte, error) { + return g.MarshalJSONOpts() +} + +func (g GRPC[T]) MarshalJSONOpts(opts ...util.Mod[protojson.MarshalOptions]) ([]byte, error) { return Marshal[T](g.Object, opts...) }