From b5e707e2afdd386dac22b3fcc697862029d594b4 Mon Sep 17 00:00:00 2001 From: Adam Janikowski <12255597+ajanikow@users.noreply.github.com> Date: Wed, 29 Dec 2021 13:06:34 +0100 Subject: [PATCH] [Feature] License V2 for ArangoDB 3.9.0+ (#870) --- CHANGELOG.md | 1 + pkg/apis/deployment/v1/conditions.go | 97 +++++++++----- pkg/apis/deployment/v1/plan.go | 6 +- pkg/apis/deployment/v2alpha1/conditions.go | 97 +++++++++----- pkg/apis/deployment/v2alpha1/plan.go | 6 +- pkg/deployment/client/client.go | 2 + pkg/deployment/client/license.go | 89 +++++++++++++ .../reconcile/action_set_condition_v2.go | 104 +++++++++++++++ .../reconcile/action_set_license.go | 120 ++++++++++++++++++ .../action_set_member_condition_v2.go | 102 +++++++++++++++ pkg/deployment/reconcile/plan_builder.go | 1 + .../reconcile/plan_builder_appender.go | 14 ++ pkg/deployment/reconcile/plan_builder_high.go | 1 + .../reconcile/plan_builder_license.go | 89 +++++++++++++ .../reconcile/plan_builder_utils.go | 23 ++++ .../resources/pod_creator_arangod.go | 2 +- pkg/util/arangod/license.go | 27 ++++ pkg/util/constants/constants.go | 6 +- pkg/util/k8sutil/license.go | 63 +++++++++ 19 files changed, 779 insertions(+), 71 deletions(-) create mode 100644 pkg/deployment/client/license.go create mode 100644 pkg/deployment/reconcile/action_set_condition_v2.go create mode 100644 pkg/deployment/reconcile/action_set_license.go create mode 100644 pkg/deployment/reconcile/action_set_member_condition_v2.go create mode 100644 pkg/deployment/reconcile/plan_builder_license.go create mode 100644 pkg/util/arangod/license.go create mode 100644 pkg/util/k8sutil/license.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 695c5c7b6..1b0ae4cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Keep only recent terminations - Add endpoint into member status - Add debug mode (Golang DLV) +- License V2 for ArangoDB 3.9.0+ ## [1.2.6](https://github.com/arangodb/kube-arangodb/tree/1.2.6) (2021-12-15) - Add ArangoBackup backoff functionality diff --git a/pkg/apis/deployment/v1/conditions.go b/pkg/apis/deployment/v1/conditions.go index 505451ac5..0fa4ef86c 100644 --- a/pkg/apis/deployment/v1/conditions.go +++ b/pkg/apis/deployment/v1/conditions.go @@ -84,6 +84,9 @@ const ( // ConditionTypeTopologyAware indicates that the member is deployed with TopologyAwareness. ConditionTypeTopologyAware ConditionType = "TopologyAware" + + // ConditionTypeLicenseSet indicates that license V2 is set on cluster. + ConditionTypeLicenseSet ConditionType = "LicenseSet" ) // Condition represents one current condition of a deployment or deployment member. @@ -102,6 +105,8 @@ type Condition struct { Reason string `json:"reason,omitempty"` // A human readable message indicating details about the transition. Message string `json:"message,omitempty"` + // Hash keep propagation hash id, for example checksum of secret + Hash string `json:"hash,omitempty"` } func (c Condition) IsTrue() bool { @@ -139,7 +144,8 @@ func (c Condition) Equal(other Condition) bool { util.TimeCompareEqual(c.LastUpdateTime, other.LastUpdateTime) && util.TimeCompareEqual(c.LastTransitionTime, other.LastTransitionTime) && c.Reason == other.Reason && - c.Message == other.Message + c.Message == other.Message && + c.Hash == other.Hash } // IsTrue return true when a condition with given type exists and its status is `True`. @@ -178,47 +184,72 @@ func (list *ConditionList) Touch(conditionType ConditionType) bool { return false } -// Update the condition, replacing an old condition with same type (if any) -// Returns true when changes were made, false otherwise. -func (list *ConditionList) Update(conditionType ConditionType, status bool, reason, message string) bool { +func (list ConditionList) Index(conditionType ConditionType) int { + for i, x := range list { + if x.Type == conditionType { + return i + } + } + + return -1 +} + +func (list *ConditionList) update(conditionType ConditionType, status bool, reason, message, hash string) bool { src := *list statusX := v1.ConditionFalse if status { statusX = v1.ConditionTrue } - for i, x := range src { - if x.Type == conditionType { - if x.Status != statusX { - // Transition to another status - src[i].Status = statusX - now := metav1.Now() - src[i].LastTransitionTime = now - src[i].LastUpdateTime = now - src[i].Reason = reason - src[i].Message = message - } else if x.Reason != reason || x.Message != message { - src[i].LastUpdateTime = metav1.Now() - src[i].Reason = reason - src[i].Message = message - } else { - return false - } - return true - } + + index := list.Index(conditionType) + + if index == -1 { + // Not found + now := metav1.Now() + *list = append(src, Condition{ + Type: conditionType, + LastUpdateTime: now, + LastTransitionTime: now, + Status: statusX, + Reason: reason, + Message: message, + Hash: hash, + }) + return true + } + + if src[index].Status != statusX { + // Transition to another status + src[index].Status = statusX + now := metav1.Now() + src[index].LastTransitionTime = now + src[index].LastUpdateTime = now + src[index].Reason = reason + src[index].Message = message + src[index].Hash = hash + } else if src[index].Reason != reason || src[index].Message != message || src[index].Hash != hash { + src[index].LastUpdateTime = metav1.Now() + src[index].Reason = reason + src[index].Message = message + src[index].Hash = hash + } else { + return false } - // Not found - now := metav1.Now() - *list = append(src, Condition{ - Type: conditionType, - LastUpdateTime: now, - LastTransitionTime: now, - Status: statusX, - Reason: reason, - Message: message, - }) return true } +// Update the condition, replacing an old condition with same type (if any) +// Returns true when changes were made, false otherwise. +func (list *ConditionList) Update(conditionType ConditionType, status bool, reason, message string) bool { + return list.update(conditionType, status, reason, message, "") +} + +// UpdateWithHash updates the condition, replacing an old condition with same type (if any) +// Returns true when changes were made, false otherwise. +func (list *ConditionList) UpdateWithHash(conditionType ConditionType, status bool, reason, message, hash string) bool { + return list.update(conditionType, status, reason, message, hash) +} + // Remove the condition with given type. // Returns true if removed, or false if not found. func (list *ConditionList) Remove(conditionType ConditionType) bool { diff --git a/pkg/apis/deployment/v1/plan.go b/pkg/apis/deployment/v1/plan.go index 0ddafc218..9a375630c 100644 --- a/pkg/apis/deployment/v1/plan.go +++ b/pkg/apis/deployment/v1/plan.go @@ -49,7 +49,7 @@ func (a ActionType) String() string { // Priority returns plan priority func (a ActionType) Priority() ActionPriority { switch a { - case ActionTypeMemberPhaseUpdate, ActionTypeMemberRIDUpdate, ActionTypeSetMemberCondition, ActionTypeSetCondition: + case ActionTypeMemberPhaseUpdate, ActionTypeMemberRIDUpdate, ActionTypeSetMemberCondition, ActionTypeSetCondition, ActionTypeSetMemberConditionV2: return ActionPriorityHigh default: return ActionPriorityNormal @@ -161,8 +161,12 @@ const ( ActionTypeMemberPhaseUpdate ActionType = "MemberPhaseUpdate" // ActionTypeSetMemberCondition sets member condition. It is high priority action. ActionTypeSetMemberCondition ActionType = "SetMemberCondition" + // ActionTypeSetMemberConditionV2 sets member condition. It is high priority action. + ActionTypeSetMemberConditionV2 ActionType = "SetMemberConditionV2" // ActionTypeSetCondition sets condition. It is high priority action. ActionTypeSetCondition ActionType = "SetCondition" + // ActionTypeSetConditionV2 sets condition. It is high priority action. + ActionTypeSetConditionV2 ActionType = "SetConditionV2" // ActionTypeMemberRIDUpdate updated member Run ID (UID). High priority ActionTypeMemberRIDUpdate ActionType = "MemberRIDUpdate" // ActionTypeArangoMemberUpdatePodSpec updates pod spec diff --git a/pkg/apis/deployment/v2alpha1/conditions.go b/pkg/apis/deployment/v2alpha1/conditions.go index 2f2d33b42..c0e17e1f5 100644 --- a/pkg/apis/deployment/v2alpha1/conditions.go +++ b/pkg/apis/deployment/v2alpha1/conditions.go @@ -84,6 +84,9 @@ const ( // ConditionTypeTopologyAware indicates that the member is deployed with TopologyAwareness. ConditionTypeTopologyAware ConditionType = "TopologyAware" + + // ConditionTypeLicenseSet indicates that license V2 is set on cluster. + ConditionTypeLicenseSet ConditionType = "LicenseSet" ) // Condition represents one current condition of a deployment or deployment member. @@ -102,6 +105,8 @@ type Condition struct { Reason string `json:"reason,omitempty"` // A human readable message indicating details about the transition. Message string `json:"message,omitempty"` + // Hash keep propagation hash id, for example checksum of secret + Hash string `json:"hash,omitempty"` } func (c Condition) IsTrue() bool { @@ -139,7 +144,8 @@ func (c Condition) Equal(other Condition) bool { util.TimeCompareEqual(c.LastUpdateTime, other.LastUpdateTime) && util.TimeCompareEqual(c.LastTransitionTime, other.LastTransitionTime) && c.Reason == other.Reason && - c.Message == other.Message + c.Message == other.Message && + c.Hash == other.Hash } // IsTrue return true when a condition with given type exists and its status is `True`. @@ -178,47 +184,72 @@ func (list *ConditionList) Touch(conditionType ConditionType) bool { return false } -// Update the condition, replacing an old condition with same type (if any) -// Returns true when changes were made, false otherwise. -func (list *ConditionList) Update(conditionType ConditionType, status bool, reason, message string) bool { +func (list ConditionList) Index(conditionType ConditionType) int { + for i, x := range list { + if x.Type == conditionType { + return i + } + } + + return -1 +} + +func (list *ConditionList) update(conditionType ConditionType, status bool, reason, message, hash string) bool { src := *list statusX := v1.ConditionFalse if status { statusX = v1.ConditionTrue } - for i, x := range src { - if x.Type == conditionType { - if x.Status != statusX { - // Transition to another status - src[i].Status = statusX - now := metav1.Now() - src[i].LastTransitionTime = now - src[i].LastUpdateTime = now - src[i].Reason = reason - src[i].Message = message - } else if x.Reason != reason || x.Message != message { - src[i].LastUpdateTime = metav1.Now() - src[i].Reason = reason - src[i].Message = message - } else { - return false - } - return true - } + + index := list.Index(conditionType) + + if index == -1 { + // Not found + now := metav1.Now() + *list = append(src, Condition{ + Type: conditionType, + LastUpdateTime: now, + LastTransitionTime: now, + Status: statusX, + Reason: reason, + Message: message, + Hash: hash, + }) + return true + } + + if src[index].Status != statusX { + // Transition to another status + src[index].Status = statusX + now := metav1.Now() + src[index].LastTransitionTime = now + src[index].LastUpdateTime = now + src[index].Reason = reason + src[index].Message = message + src[index].Hash = hash + } else if src[index].Reason != reason || src[index].Message != message || src[index].Hash != hash { + src[index].LastUpdateTime = metav1.Now() + src[index].Reason = reason + src[index].Message = message + src[index].Hash = hash + } else { + return false } - // Not found - now := metav1.Now() - *list = append(src, Condition{ - Type: conditionType, - LastUpdateTime: now, - LastTransitionTime: now, - Status: statusX, - Reason: reason, - Message: message, - }) return true } +// Update the condition, replacing an old condition with same type (if any) +// Returns true when changes were made, false otherwise. +func (list *ConditionList) Update(conditionType ConditionType, status bool, reason, message string) bool { + return list.update(conditionType, status, reason, message, "") +} + +// UpdateWithHash updates the condition, replacing an old condition with same type (if any) +// Returns true when changes were made, false otherwise. +func (list *ConditionList) UpdateWithHash(conditionType ConditionType, status bool, reason, message, hash string) bool { + return list.update(conditionType, status, reason, message, hash) +} + // Remove the condition with given type. // Returns true if removed, or false if not found. func (list *ConditionList) Remove(conditionType ConditionType) bool { diff --git a/pkg/apis/deployment/v2alpha1/plan.go b/pkg/apis/deployment/v2alpha1/plan.go index 0f2527625..c7f31230b 100644 --- a/pkg/apis/deployment/v2alpha1/plan.go +++ b/pkg/apis/deployment/v2alpha1/plan.go @@ -49,7 +49,7 @@ func (a ActionType) String() string { // Priority returns plan priority func (a ActionType) Priority() ActionPriority { switch a { - case ActionTypeMemberPhaseUpdate, ActionTypeMemberRIDUpdate, ActionTypeSetMemberCondition, ActionTypeSetCondition: + case ActionTypeMemberPhaseUpdate, ActionTypeMemberRIDUpdate, ActionTypeSetMemberCondition, ActionTypeSetCondition, ActionTypeSetMemberConditionV2: return ActionPriorityHigh default: return ActionPriorityNormal @@ -161,8 +161,12 @@ const ( ActionTypeMemberPhaseUpdate ActionType = "MemberPhaseUpdate" // ActionTypeSetMemberCondition sets member condition. It is high priority action. ActionTypeSetMemberCondition ActionType = "SetMemberCondition" + // ActionTypeSetMemberConditionV2 sets member condition. It is high priority action. + ActionTypeSetMemberConditionV2 ActionType = "SetMemberConditionV2" // ActionTypeSetCondition sets condition. It is high priority action. ActionTypeSetCondition ActionType = "SetCondition" + // ActionTypeSetConditionV2 sets condition. It is high priority action. + ActionTypeSetConditionV2 ActionType = "SetConditionV2" // ActionTypeMemberRIDUpdate updated member Run ID (UID). High priority ActionTypeMemberRIDUpdate ActionType = "MemberRIDUpdate" // ActionTypeArangoMemberUpdatePodSpec updates pod spec diff --git a/pkg/deployment/client/client.go b/pkg/deployment/client/client.go index 45a842b02..3ecfa8797 100644 --- a/pkg/deployment/client/client.go +++ b/pkg/deployment/client/client.go @@ -36,6 +36,8 @@ func NewClient(c driver.Connection) Client { } type Client interface { + LicenseClient + GetTLS(ctx context.Context) (TLSDetails, error) RefreshTLS(ctx context.Context) (TLSDetails, error) diff --git a/pkg/deployment/client/license.go b/pkg/deployment/client/license.go new file mode 100644 index 000000000..e06b55e9b --- /dev/null +++ b/pkg/deployment/client/license.go @@ -0,0 +1,89 @@ +// +// DISCLAIMER +// +// Copyright 2016-2021 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" + "net/http" +) + +const AdminLicenseUrl = "/_admin/license" + +type LicenseClient interface { + GetLicense(ctx context.Context) (License, error) + SetLicense(ctx context.Context, license string, force bool) error +} + +type License struct { + Hash string `json:"hash,omitempty"` +} + +func (c *client) GetLicense(ctx context.Context) (License, error) { + req, err := c.c.NewRequest(http.MethodGet, AdminLicenseUrl) + if err != nil { + return License{}, err + } + + resp, err := c.c.Do(ctx, req) + if err != nil { + return License{}, err + } + + if err := resp.CheckStatus(http.StatusOK); err != nil { + return License{}, err + } + + var l License + + if err := resp.ParseBody("", &l); err != nil { + return License{}, err + } + + return l, nil +} + +func (c *client) SetLicense(ctx context.Context, license string, force bool) error { + req, err := c.c.NewRequest(http.MethodPut, AdminLicenseUrl) + if err != nil { + return err + } + + if r, err := req.SetBody(license); err != nil { + return err + } else { + req = r + } + + if force { + req = req.SetQuery("force", "true") + } + + resp, err := c.c.Do(ctx, req) + if err != nil { + return err + } + + if err := resp.CheckStatus(http.StatusCreated); err != nil { + return err + } + + return nil +} diff --git a/pkg/deployment/reconcile/action_set_condition_v2.go b/pkg/deployment/reconcile/action_set_condition_v2.go new file mode 100644 index 000000000..bdb43e312 --- /dev/null +++ b/pkg/deployment/reconcile/action_set_condition_v2.go @@ -0,0 +1,104 @@ +// +// DISCLAIMER +// +// Copyright 2016-2021 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 +// +// Author Tomasz Mielech +// + +package reconcile + +import ( + "context" + + "github.com/rs/zerolog" + core "k8s.io/api/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" +) + +func init() { + registerAction(api.ActionTypeSetConditionV2, setConditionV2) +} + +const ( + setConditionActionV2KeyTypeAdd string = "add" + setConditionActionV2KeyTypeRemove string = "remove" + + setConditionActionV2KeyType string = "type" + setConditionActionV2KeyAction string = "action" + setConditionActionV2KeyStatus string = "status" + setConditionActionV2KeyReason string = "reason" + setConditionActionV2KeyMessage string = "message" + setConditionActionV2KeyHash string = "hash" +) + +func setConditionV2(log zerolog.Logger, action api.Action, actionCtx ActionContext) Action { + a := &actionSetConditionV2{} + + a.actionImpl = newActionImplDefRef(log, action, actionCtx, defaultTimeout) + + return a +} + +type actionSetConditionV2 struct { + // actionImpl implement timeout and member id functions + actionImpl + + actionEmptyCheckProgress +} + +// Start starts the action for changing conditions on the provided member. +func (a actionSetConditionV2) Start(ctx context.Context) (bool, error) { + at, ok := a.action.Params[setConditionActionV2KeyType] + if !ok { + a.log.Info().Msgf("key %s is missing in action definition", setConditionActionV2KeyType) + return true, nil + } + + aa, ok := a.action.Params[setConditionActionV2KeyAction] + if !ok { + a.log.Info().Msgf("key %s is missing in action definition", setConditionActionV2KeyAction) + return true, nil + } + + switch at { + case setConditionActionV2KeyTypeAdd: + ah := a.action.Params[setConditionActionV2KeyHash] + am := a.action.Params[setConditionActionV2KeyMessage] + ar := a.action.Params[setConditionActionV2KeyReason] + as := a.action.Params[setConditionActionV2KeyStatus] == string(core.ConditionTrue) + + if err := a.actionCtx.WithStatusUpdateErr(ctx, func(s *api.DeploymentStatus) (bool, error) { + return s.Conditions.UpdateWithHash(api.ConditionType(aa), as, ar, am, ah), nil + }); err != nil { + a.log.Warn().Err(err).Msgf("unable to update status") + return true, nil + } + case setConditionActionV2KeyTypeRemove: + if err := a.actionCtx.WithStatusUpdateErr(ctx, func(s *api.DeploymentStatus) (bool, error) { + return s.Conditions.Remove(api.ConditionType(aa)), nil + }); err != nil { + a.log.Warn().Err(err).Msgf("unable to update status") + return true, nil + } + default: + a.log.Info().Msgf("unknown type %s", at) + return true, nil + } + return true, nil +} diff --git a/pkg/deployment/reconcile/action_set_license.go b/pkg/deployment/reconcile/action_set_license.go new file mode 100644 index 000000000..63ae47335 --- /dev/null +++ b/pkg/deployment/reconcile/action_set_license.go @@ -0,0 +1,120 @@ +// +// DISCLAIMER +// +// Copyright 2020-2021 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 +// +// Author Adam Janikowski +// + +package reconcile + +import ( + "context" + + "github.com/arangodb/kube-arangodb/pkg/util/globals" + + "github.com/arangodb/kube-arangodb/pkg/deployment/client" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/rs/zerolog" +) + +func init() { + registerAction(api.ActionTypeLicenseSet, newLicenseSet) +} + +func newLicenseSet(log zerolog.Logger, action api.Action, actionCtx ActionContext) Action { + a := &licenseSetAction{} + + a.actionImpl = newActionImplDefRef(log, action, actionCtx, defaultTimeout) + + return a +} + +type licenseSetAction struct { + actionImpl + + actionEmptyCheckProgress +} + +func (a *licenseSetAction) Start(ctx context.Context) (bool, error) { + log := a.log + + spec := a.actionCtx.GetSpec() + if !spec.License.HasSecretName() { + log.Error().Msg("License is not set") + return true, nil + } + + l, ok := k8sutil.GetLicenseFromSecret(a.actionCtx.GetCachedStatus(), spec.License.GetSecretName()) + + if !ok { + return true, nil + } + + if !l.V2.IsV2Set() { + return true, nil + } + + group := a.action.Group + m, ok := a.actionCtx.GetMemberStatusByID(a.action.MemberID) + if !ok { + log.Error().Msg("No such member") + return true, nil + } + + ctxChild, cancel := globals.GetGlobals().Timeouts().ArangoD().WithTimeout(ctx) + defer cancel() + + c, err := a.actionCtx.GetServerClient(ctxChild, group, m.ID) + if !ok { + log.Error().Err(err).Msg("Unable to get client") + return true, nil + } + + client := client.NewClient(c.Connection()) + + if ok, err := licenseV2Compare(ctx, client, l.V2); err != nil { + log.Error().Err(err).Msg("Unable to verify license") + return true, nil + } else if ok { + // Already latest license + return true, nil + } + + if err := client.SetLicense(ctx, string(l.V2), true); err != nil { + log.Error().Err(err).Msg("Unable to set license") + return true, nil + } + + return true, nil +} + +func licenseV2Compare(ctx context.Context, client client.Client, license k8sutil.License) (bool, error) { + currentLicense, err := client.GetLicense(ctx) + if err != nil { + return false, err + } + + if currentLicense.Hash == license.V2Hash() { + // Already latest license + return true, nil + } + + return false, nil +} diff --git a/pkg/deployment/reconcile/action_set_member_condition_v2.go b/pkg/deployment/reconcile/action_set_member_condition_v2.go new file mode 100644 index 000000000..ea05531c6 --- /dev/null +++ b/pkg/deployment/reconcile/action_set_member_condition_v2.go @@ -0,0 +1,102 @@ +// +// DISCLAIMER +// +// Copyright 2016-2021 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" + + "github.com/rs/zerolog" + core "k8s.io/api/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" +) + +func init() { + registerAction(api.ActionTypeSetMemberConditionV2, setMemberConditionV2) +} + +func setMemberConditionV2(log zerolog.Logger, action api.Action, actionCtx ActionContext) Action { + a := &actionSetMemberConditionV2{} + + a.actionImpl = newActionImplDefRef(log, action, actionCtx, defaultTimeout) + + return a +} + +type actionSetMemberConditionV2 struct { + // actionImpl implement timeout and member id functions + actionImpl + + actionEmptyCheckProgress +} + +// Start starts the action for changing conditions on the provided member. +func (a actionSetMemberConditionV2) Start(ctx context.Context) (bool, error) { + at, ok := a.action.Params[setConditionActionV2KeyType] + if !ok { + a.log.Info().Msgf("key %s is missing in action definition", setConditionActionV2KeyType) + return true, nil + } + + aa, ok := a.action.Params[setConditionActionV2KeyAction] + if !ok { + a.log.Info().Msgf("key %s is missing in action definition", setConditionActionV2KeyAction) + return true, nil + } + + switch at { + case setConditionActionV2KeyTypeAdd: + ah := a.action.Params[setConditionActionV2KeyHash] + am := a.action.Params[setConditionActionV2KeyMessage] + ar := a.action.Params[setConditionActionV2KeyReason] + as := a.action.Params[setConditionActionV2KeyStatus] == string(core.ConditionTrue) + + if err := a.actionCtx.WithStatusUpdateErr(ctx, func(s *api.DeploymentStatus) (bool, error) { + m, _, ok := s.Members.ElementByID(a.action.MemberID) + if !ok { + a.log.Info().Msg("can not set the condition because the member is gone already") + return false, nil + } + + return m.Conditions.UpdateWithHash(api.ConditionType(aa), as, ar, am, ah), nil + }); err != nil { + a.log.Warn().Err(err).Msgf("unable to update status") + return true, nil + } + case setConditionActionV2KeyTypeRemove: + if err := a.actionCtx.WithStatusUpdateErr(ctx, func(s *api.DeploymentStatus) (bool, error) { + m, _, ok := s.Members.ElementByID(a.action.MemberID) + if !ok { + a.log.Info().Msg("can not set the condition because the member is gone already") + return false, nil + } + + return m.Conditions.Remove(api.ConditionType(aa)), nil + }); err != nil { + a.log.Warn().Err(err).Msgf("unable to update status") + return true, nil + } + default: + a.log.Info().Msgf("unknown type %s", at) + return true, nil + } + return true, nil +} diff --git a/pkg/deployment/reconcile/plan_builder.go b/pkg/deployment/reconcile/plan_builder.go index e21a03600..2fade30b5 100644 --- a/pkg/deployment/reconcile/plan_builder.go +++ b/pkg/deployment/reconcile/plan_builder.go @@ -37,6 +37,7 @@ const ( const ( BackOffCheck api.BackOffKey = "check" + LicenseCheck api.BackOffKey = "license" ) // CreatePlan considers the current specification & status of the deployment creates a plan to diff --git a/pkg/deployment/reconcile/plan_builder_appender.go b/pkg/deployment/reconcile/plan_builder_appender.go index 956f0bd7e..7c0a87165 100644 --- a/pkg/deployment/reconcile/plan_builder_appender.go +++ b/pkg/deployment/reconcile/plan_builder_appender.go @@ -54,6 +54,7 @@ type PlanAppender interface { ApplySubPlanIfEmpty(pb planBuilderSubPlan, plans ...planBuilder) PlanAppender ApplyWithBackOff(key api.BackOffKey, delay time.Duration, pb planBuilder) PlanAppender + ApplyIfEmptyWithBackOff(key api.BackOffKey, delay time.Duration, pb planBuilder) PlanAppender BackOff() api.BackOff @@ -75,6 +76,12 @@ func (p planAppenderRecovery) ApplyWithBackOff(key api.BackOffKey, delay time.Du }) } +func (p planAppenderRecovery) ApplyIfEmptyWithBackOff(key api.BackOffKey, delay time.Duration, pb planBuilder) PlanAppender { + return p.create(func(in PlanAppender) PlanAppender { + return in.ApplyIfEmptyWithBackOff(key, delay, pb) + }) +} + func (p planAppenderRecovery) create(ret func(in PlanAppender) PlanAppender) (r PlanAppender) { defer func() { if e := recover(); e != nil { @@ -150,6 +157,13 @@ func (p *planAppenderType) ApplyWithBackOff(key api.BackOffKey, delay time.Durat return p.Apply(pb) } +func (p *planAppenderType) ApplyIfEmptyWithBackOff(key api.BackOffKey, delay time.Duration, pb planBuilder) PlanAppender { + if p.current.IsEmpty() { + return p.ApplyWithBackOff(key, delay, pb) + } + return p +} + func (p *planAppenderType) Plan() api.Plan { return p.current } diff --git a/pkg/deployment/reconcile/plan_builder_high.go b/pkg/deployment/reconcile/plan_builder_high.go index 04c61cde6..befde3d1c 100644 --- a/pkg/deployment/reconcile/plan_builder_high.go +++ b/pkg/deployment/reconcile/plan_builder_high.go @@ -54,6 +54,7 @@ func createHighPlan(ctx context.Context, log zerolog.Logger, apiObject k8sutil.A ApplyIfEmpty(updateMemberUpdateConditionsPlan). ApplyIfEmpty(updateMemberRotationConditionsPlan). ApplyIfEmpty(createTopologyMemberUpdatePlan). + ApplyIfEmptyWithBackOff(LicenseCheck, 30*time.Second, updateClusterLicense). ApplyIfEmpty(createTopologyMemberConditionPlan). ApplyIfEmpty(createRebalancerCheckPlan). ApplyWithBackOff(BackOffCheck, time.Minute, emptyPlanBuilder)) diff --git a/pkg/deployment/reconcile/plan_builder_license.go b/pkg/deployment/reconcile/plan_builder_license.go new file mode 100644 index 000000000..b9bfa3322 --- /dev/null +++ b/pkg/deployment/reconcile/plan_builder_license.go @@ -0,0 +1,89 @@ +// +// DISCLAIMER +// +// Copyright 2016-2021 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" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/deployment/client" + "github.com/arangodb/kube-arangodb/pkg/util/arangod" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + inspectorInterface "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector" + "github.com/rs/zerolog" +) + +func updateClusterLicense(ctx context.Context, + log zerolog.Logger, apiObject k8sutil.APIObject, + spec api.DeploymentSpec, status api.DeploymentStatus, + cachedStatus inspectorInterface.Inspector, context PlanBuilderContext) api.Plan { + if !spec.License.HasSecretName() { + return nil + } + + l, ok := k8sutil.GetLicenseFromSecret(context.GetCachedStatus(), spec.License.GetSecretName()) + if !ok { + log.Trace().Str("secret", spec.Authentication.GetJWTSecretName()).Msgf("Unable to find license secret key") + return nil + } + + if !l.V2.IsV2Set() { + log.Trace().Str("secret", spec.Authentication.GetJWTSecretName()).Msgf("V2 License key is not set") + return nil + } + + members := status.Members.AsListInGroups(arangod.GroupsWithLicenseV2()...).Filter(func(a api.DeploymentStatusMemberElement) bool { + i := a.Member.Image + if i == nil { + return false + } + + return i.ArangoDBVersion.CompareTo("3.9.0") >= 0 + }) + + if len(members) == 0 { + // No member found to take this action + log.Trace().Msgf("No member in version 3.9.0 or above") + return nil + } + + member := members[0] + + c, err := context.GetServerClient(ctx, member.Group, member.Member.ID) + if err != nil { + log.Err(err).Msgf("Unable to get client") + return nil + } + + internalClient := client.NewClient(c.Connection()) + + if ok, err := licenseV2Compare(ctx, internalClient, l.V2); err != nil { + log.Error().Err(err).Msg("Unable to verify license") + return nil + } else if ok { + if c, _ := status.Conditions.Get(api.ConditionTypeLicenseSet); !c.IsTrue() || c.Hash != l.V2.V2Hash() { + return api.Plan{updateConditionActionV2("License is set", api.ConditionTypeLicenseSet, true, "License UpToDate", "", l.V2.V2Hash())} + } + return nil + } + + return api.Plan{removeConditionActionV2("License is not set", api.ConditionTypeLicenseSet), api.NewAction(api.ActionTypeLicenseSet, member.Group, member.Member.ID, "Setting license")} +} diff --git a/pkg/deployment/reconcile/plan_builder_utils.go b/pkg/deployment/reconcile/plan_builder_utils.go index ef9e79dbc..57e57c3f0 100644 --- a/pkg/deployment/reconcile/plan_builder_utils.go +++ b/pkg/deployment/reconcile/plan_builder_utils.go @@ -25,6 +25,8 @@ package reconcile import ( "context" + core "k8s.io/api/core/v1" + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" inspectorInterface "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector" @@ -57,3 +59,24 @@ func emptyPlanBuilder(ctx context.Context, cachedStatus inspectorInterface.Inspector, context PlanBuilderContext) api.Plan { return nil } + +func removeConditionActionV2(actionReason string, conditionType api.ConditionType) api.Action { + return api.NewAction(api.ActionTypeSetConditionV2, api.ServerGroupUnknown, "", actionReason). + AddParam(setConditionActionV2KeyAction, setConditionActionV2KeyTypeRemove). + AddParam(setConditionActionV2KeyType, string(conditionType)) +} + +func updateConditionActionV2(actionReason string, conditionType api.ConditionType, status bool, reason, message, hash string) api.Action { + statusBool := core.ConditionTrue + if !status { + statusBool = core.ConditionFalse + } + + return api.NewAction(api.ActionTypeSetConditionV2, api.ServerGroupUnknown, "", actionReason). + AddParam(setConditionActionV2KeyAction, string(conditionType)). + AddParam(setConditionActionV2KeyType, setConditionActionV2KeyTypeAdd). + AddParam(setConditionActionV2KeyStatus, string(statusBool)). + AddParam(setConditionActionV2KeyReason, reason). + AddParam(setConditionActionV2KeyMessage, message). + AddParam(setConditionActionV2KeyHash, hash) +} diff --git a/pkg/deployment/resources/pod_creator_arangod.go b/pkg/deployment/resources/pod_creator_arangod.go index 90aad9ffb..56cd7e3b7 100644 --- a/pkg/deployment/resources/pod_creator_arangod.go +++ b/pkg/deployment/resources/pod_creator_arangod.go @@ -182,7 +182,7 @@ func (a *ArangoDContainer) GetImage() string { func (a *ArangoDContainer) GetEnvs() []core.EnvVar { envs := NewEnvBuilder() - if a.spec.License.HasSecretName() { + if a.spec.License.HasSecretName() && a.imageInfo.ArangoDBVersion.CompareTo("3.9.0") < 0 { env := k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoLicenseKey, a.spec.License.GetSecretName(), constants.SecretKeyToken) diff --git a/pkg/util/arangod/license.go b/pkg/util/arangod/license.go new file mode 100644 index 000000000..7d078e326 --- /dev/null +++ b/pkg/util/arangod/license.go @@ -0,0 +1,27 @@ +// +// DISCLAIMER +// +// Copyright 2016-2021 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 arangod + +import api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + +func GroupsWithLicenseV2() api.ServerGroups { + return api.ServerGroups{api.ServerGroupSingle, api.ServerGroupDBServers, api.ServerGroupCoordinators} +} diff --git a/pkg/util/constants/constants.go b/pkg/util/constants/constants.go index b7742d526..817115d8b 100644 --- a/pkg/util/constants/constants.go +++ b/pkg/util/constants/constants.go @@ -33,8 +33,10 @@ const ( EnvArangoLicenseKey = "ARANGO_LICENSE_KEY" // Contains the License Key for the Docker Image EnvArangoSyncMonitoringToken = "ARANGOSYNC_MONITORING_TOKEN" // Constains monitoring token for ArangoSync servers - SecretEncryptionKey = "key" // Key in a Secret.Data used to store an 32-byte encryption key - SecretKeyToken = "token" // Key inside a Secret used to hold a JWT or monitoring token + SecretEncryptionKey = "key" // Key in a Secret.Data used to store an 32-byte encryption key + SecretKeyToken = "token" // Key inside a Secret used to hold a JWT or monitoring token + SecretKeyV2Token = "token-v2" // Key inside a Secret used to hold a License in V2 Format + SecretKeyV2License = "license-v2" // Key inside a Secret used to hold a License in V2 Format SecretCACertificate = "ca.crt" // Key in Secret.data used to store a PEM encoded CA certificate (public key) SecretCAKey = "ca.key" // Key in Secret.data used to store a PEM encoded CA private key diff --git a/pkg/util/k8sutil/license.go b/pkg/util/k8sutil/license.go new file mode 100644 index 000000000..5c317e6f5 --- /dev/null +++ b/pkg/util/k8sutil/license.go @@ -0,0 +1,63 @@ +// +// DISCLAIMER +// +// Copyright 2016-2021 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 k8sutil + +import ( + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/constants" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/secret" +) + +type License string + +func (l License) IsV2Set() bool { + return l != "" +} + +func (l License) V2Hash() string { + return util.SHA256FromString(string(l)) +} + +type LicenseSecret struct { + V1 string + V2 License +} + +func GetLicenseFromSecret(secret secret.Inspector, name string) (LicenseSecret, bool) { + s, ok := secret.Secret(name) + if !ok { + return LicenseSecret{}, false + } + + var l LicenseSecret + + if v, ok := s.Data[constants.SecretKeyToken]; ok { + l.V1 = string(v) + } + + if v1, ok1 := s.Data[constants.SecretKeyV2License]; ok1 { + l.V2 = License(v1) + } else if v2, ok2 := s.Data[constants.SecretKeyV2Token]; ok2 { + l.V2 = License(v2) + } + + return l, true +}