Skip to content

Commit ac5b901

Browse files
feat: cf account validate-limits command (#785)
## What A new command has been added ## Why To validate account limits
1 parent d0c8b0c commit ac5b901

File tree

15 files changed

+639
-36
lines changed

15 files changed

+639
-36
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION=v0.2.6
1+
VERSION=v0.2.7
22

33
OUT_DIR=dist
44
YEAR?=$(shell date +"%Y")

cmd/commands/account.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright 2025 The Codefresh Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package commands
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"github.com/codefresh-io/cli-v2/internal/kube"
21+
"github.com/codefresh-io/cli-v2/internal/log"
22+
"github.com/codefresh-io/cli-v2/internal/util"
23+
"github.com/codefresh-io/cli-v2/internal/util/helm"
24+
"github.com/codefresh-io/go-sdk/pkg/codefresh"
25+
"github.com/codefresh-io/go-sdk/pkg/graphql"
26+
platmodel "github.com/codefresh-io/go-sdk/pkg/model/platform"
27+
"github.com/spf13/cobra"
28+
"strings"
29+
)
30+
31+
type (
32+
ValidateLimitsOptions struct {
33+
HelmValidateValuesOptions
34+
failCondition string
35+
subject string
36+
}
37+
)
38+
39+
const (
40+
failConditionReached = "reached"
41+
failConditionExceeded = "exceeded"
42+
43+
subjectClusters = "clusters"
44+
subjectApplications = "applications"
45+
)
46+
47+
func NewAccountCommand() *cobra.Command {
48+
cmd := &cobra.Command{
49+
Use: "account",
50+
Short: "Account related commands",
51+
Args: cobra.NoArgs, // Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706
52+
Run: func(cmd *cobra.Command, args []string) {
53+
cmd.HelpFunc()(cmd, args)
54+
exit(1)
55+
},
56+
}
57+
58+
cmd.AddCommand(NewValidateLimitsCommand())
59+
60+
return cmd
61+
}
62+
63+
func NewValidateLimitsCommand() *cobra.Command {
64+
opts := &ValidateLimitsOptions{}
65+
66+
cmd := &cobra.Command{
67+
Use: "validate-usage",
68+
Aliases: []string{"vu"},
69+
Args: cobra.NoArgs,
70+
Short: "Validate usage of account resources",
71+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
72+
if opts.hook {
73+
return nil
74+
}
75+
return cfConfig.RequireAuthentication(cmd, args)
76+
},
77+
Example: util.Doc("<BIN> account validate-usage"),
78+
RunE: func(cmd *cobra.Command, _ []string) error {
79+
ctx := cmd.Context()
80+
if opts.hook {
81+
log.G(ctx).Infof("Running in hook-mode")
82+
}
83+
84+
var (
85+
client codefresh.Codefresh
86+
err error
87+
)
88+
if opts.hook {
89+
client, err = createPlatformClientInRuntime(ctx, opts)
90+
if err != nil {
91+
return err
92+
}
93+
} else {
94+
client = cfConfig.NewClient()
95+
}
96+
payments := client.GraphQL().Payments()
97+
err = runValidateLimits(cmd.Context(), opts, payments)
98+
if err != nil {
99+
return fmt.Errorf("failed validating usage: %w", err)
100+
}
101+
102+
return nil
103+
},
104+
}
105+
106+
cmd.Flags().StringVar(&opts.failCondition, "fail-condition", failConditionExceeded, "condition to validate [reached | exceeded]")
107+
cmd.Flags().StringVar(&opts.subject, "subject", "", "subject to validate [clusters | applications]. All subjects when omitted")
108+
cmd.Flags().StringVarP(&opts.valuesFile, "values", "f", "", "specify values in a YAML file or a URL")
109+
cmd.Flags().BoolVar(&opts.hook, "hook", false, "set to true when running inside a helm-hook")
110+
opts.helm, _ = helm.AddFlags(cmd.Flags())
111+
opts.kubeFactory = kube.AddFlags(cmd.Flags())
112+
113+
util.Die(cmd.Flags().MarkHidden("hook"))
114+
115+
return cmd
116+
}
117+
118+
func runValidateLimits(ctx context.Context, opts *ValidateLimitsOptions, payments graphql.PaymentsAPI) error {
119+
log.G(ctx).Info("Validating account usage")
120+
121+
limitsStatus, err := payments.GetLimitsStatus(ctx)
122+
if err != nil {
123+
return err
124+
}
125+
126+
if limitsStatus.Limits == nil {
127+
log.G(ctx).Infof("Limits are not defined for account")
128+
return nil
129+
}
130+
131+
err = ValidateGitOpsUsage(*limitsStatus.Usage, *limitsStatus.Limits, opts.failCondition, opts.subject)
132+
if err != nil {
133+
return fmt.Errorf("usage validation error: %s", err.Error())
134+
}
135+
136+
log.G(ctx).Infof("Successfully validated usage for account")
137+
return nil
138+
}
139+
140+
// ValidateGitOpsUsage checks whether the usage exceeds or reaches the defined limits.
141+
// - If 'limits' for a field is nil, validation passes for that field.
142+
// - If 'subject' is provided, only that field is checked. Otherwise, all fields are checked.
143+
// - If 'failCondition' is "reached", validation fails if usage == limit.
144+
// - If 'failCondition' is "exceeded", validation fails only if usage > limit.
145+
// - If a field in 'usage' has no corresponding field in 'limits', validation passes for that field.
146+
func ValidateGitOpsUsage(usage platmodel.GitOpsUsage, limits platmodel.GitOpsLimits, failCondition string, subject string) error {
147+
check := func(usageVal, limitVal *int, name string) error {
148+
// If usageVal is nil, return an error
149+
if usageVal == nil {
150+
return fmt.Errorf("%s usage is missing", name)
151+
}
152+
153+
// Skip validation if the limit is not set
154+
if limitVal == nil {
155+
return nil
156+
}
157+
158+
switch failCondition {
159+
case failConditionReached:
160+
if *usageVal >= *limitVal {
161+
condition := failConditionReached
162+
if *usageVal > *limitVal {
163+
condition = failConditionExceeded
164+
}
165+
return fmt.Errorf("%s limit %s: usage=%d, limit=%d", name, condition, *usageVal, *limitVal)
166+
}
167+
case failConditionExceeded:
168+
if *usageVal > *limitVal {
169+
return fmt.Errorf("%s limit exceeded: usage=%d, limit=%d", name, *usageVal, *limitVal)
170+
}
171+
default:
172+
return fmt.Errorf("invalid fail condition")
173+
}
174+
log.G().Infof("Limit checking succeeded. condition=%s, subject=%s, usage=%d, limit=%d", failCondition, subject, *usageVal, *limitVal)
175+
return nil
176+
}
177+
178+
subject = strings.ToLower(subject)
179+
validSubjects := map[string]bool{
180+
"": true,
181+
subjectApplications: true,
182+
subjectClusters: true,
183+
}
184+
185+
if !validSubjects[subject] {
186+
return fmt.Errorf("invalid subject: %s", subject)
187+
}
188+
189+
if subject == subjectApplications || subject == "" {
190+
if err := check(usage.Applications, limits.Applications, subjectApplications); err != nil {
191+
return err
192+
}
193+
}
194+
195+
if subject == subjectClusters || subject == "" {
196+
if err := check(usage.Clusters, limits.Clusters, subjectClusters); err != nil {
197+
return err
198+
}
199+
}
200+
201+
// No validation errors
202+
return nil
203+
}

0 commit comments

Comments
 (0)