Skip to content
Open
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
54 changes: 41 additions & 13 deletions cliv2/cmd/cliv2/main.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package main

// !!! This import needs to be the first import, please do not change this !!!
import _ "github.com/snyk/go-application-framework/pkg/networking/fips_enable"

import (
"context"
"encoding/json"
Expand All @@ -25,19 +23,20 @@ import (
"github.com/snyk/cli-extension-mcp-scan/pkg/mcpscan"
"github.com/snyk/cli-extension-os-flows/pkg/osflows"
"github.com/snyk/cli-extension-sbom/pkg/sbom"
"github.com/snyk/container-cli/pkg/container"
"github.com/snyk/error-catalog-golang-public/cli"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

"github.com/snyk/cli/cliv2/cmd/cliv2/behavior/legacy"
"github.com/snyk/cli/cliv2/internal/cliv2"
"github.com/snyk/cli/cliv2/internal/constants"
featureflaggateway "github.com/snyk/cli/cliv2/internal/feature-flag-gateway"
"github.com/snyk/container-cli/pkg/container"
"github.com/snyk/error-catalog-golang-public/cli"
"github.com/snyk/go-application-framework/pkg/analytics"
"github.com/snyk/go-application-framework/pkg/app"
"github.com/snyk/go-application-framework/pkg/configuration"
"github.com/snyk/go-application-framework/pkg/instrumentation"
"github.com/snyk/go-application-framework/pkg/logging"
_ "github.com/snyk/go-application-framework/pkg/networking/fips_enable"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

cliv2utils "github.com/snyk/cli/cliv2/internal/utils"

Expand All @@ -46,7 +45,6 @@ import (
"github.com/snyk/go-application-framework/pkg/local_workflows/network_utils"

workflows "github.com/snyk/go-application-framework/pkg/local_workflows/connectivity_check_extension"

"github.com/snyk/go-httpauth/pkg/httpauth"
"github.com/snyk/snyk-iac-capture/pkg/capture"

Expand All @@ -59,7 +57,6 @@ import (
"github.com/snyk/go-application-framework/pkg/workflow"

snykls "github.com/snyk/snyk-ls/ls_extension"

"github.com/snyk/studio-mcp/pkg/mcp"

cli_errors "github.com/snyk/cli/cliv2/internal/errors"
Expand All @@ -77,10 +74,11 @@ var scrubbedLogger logging.ScrubbingLogWriter
var interactionId = instrumentation.AssembleUrnFromUUID(uuid.NewString())

const (
unknownCommandMessage string = "unknown command"
disable_analytics_flag string = "DISABLE_ANALYTICS"
debug_level_flag string = "log-level"
integrationNameFlag string = "integration-name"
unknownCommandMessage string = "unknown command"
disable_analytics_flag string = "DISABLE_ANALYTICS"
debug_level_flag string = "log-level"
integrationNameFlag string = "integration-name"
showMavenBuildScopeFlag string = "show-maven-build-scope"
)

type JsonErrorStruct struct {
Expand Down Expand Up @@ -591,6 +589,14 @@ func MainWithErrorCode() int {
// add workflows as commands
createCommandsForWorkflows(rootCommand, globalEngine)

// fetch feature flags
ffgService, err := featureflaggateway.NewService(globalConfiguration.GetUrl(configuration.API_URL), 10*time.Second)
if err != nil {
globalLogger.Print("Failed to fetch feature flags", err)
return constants.SNYK_EXIT_CODE_ERROR
}
apiToken := globalConfiguration.GetString(configuration.AUTHENTICATION_TOKEN)

// init Analytics
cliAnalytics := globalEngine.GetAnalytics()
cliAnalytics.SetVersion(cliv2.GetFullVersion())
Expand All @@ -601,6 +607,8 @@ func MainWithErrorCode() int {
cliAnalytics.GetInstrumentation().SetCategory(instrumentation.DetermineCategory(os.Args, globalEngine))
cliAnalytics.GetInstrumentation().SetStage(instrumentation.DetermineStage(cliAnalytics.IsCiEnvironment()))
cliAnalytics.GetInstrumentation().SetStatus(analytics.Success)
cliAnalytics.GetInstrumentation().AddExtension(showMavenBuildScopeFlag,
IsFeatureEnabled(ctx, ffgService, globalConfiguration.GetString(configuration.ORGANIZATION), showMavenBuildScopeFlag, apiToken))

setTimeout(globalConfiguration, func() {
os.Exit(constants.SNYK_EXIT_CODE_EX_UNAVAILABLE)
Expand Down Expand Up @@ -659,6 +667,26 @@ func MainWithErrorCode() int {
return exitCode
}

func IsFeatureEnabled(
ctx context.Context,
service featureflaggateway.Service,
orgID string,
flag string,
token string,
) bool {
resp, err := service.EvaluateFlags(ctx, []string{flag}, orgID, "2024-10-15", token)
if err != nil || resp == nil {
return false
}
evals := resp.Data.Attributes.Evaluations
for _, e := range evals {
if e.Key == flag {
return e.Value
}
}
return false
}

func legacyCLITerminated(err error, errorList []error) error {
exitErr, isExitError := err.(*exec.ExitError)
if isExitError && exitErr.ExitCode() == constants.SNYK_EXIT_CODE_TS_CLI_TERMINATED {
Expand Down
14 changes: 7 additions & 7 deletions cliv2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbles v0.14.0 // indirect
Expand Down Expand Up @@ -99,7 +99,7 @@ require (
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.15.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
Expand Down Expand Up @@ -218,11 +218,11 @@ require (
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
Expand Down
28 changes: 14 additions & 14 deletions cliv2/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -691,8 +691,8 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
Expand Down Expand Up @@ -880,8 +880,8 @@ github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3I
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
Expand Down Expand Up @@ -1444,22 +1444,22 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
Expand Down
42 changes: 42 additions & 0 deletions cliv2/internal/feature-flag-gateway/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package featureflaggateway

// FeatureFlagRequest request body for feature flag request.
type FeatureFlagRequest struct {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider moving these classes to the pkg folder in the feature-flag-gateway repo so they can be reused wherever this service is used.

Data FeatureFlagsData `json:"data"`
}

// FeatureFlagsData represents feature flags response data.
type FeatureFlagsData struct {
Type string `json:"type"`
Attributes FeatureFlagsRequestAttributes `json:"attributes"`
}

// FeatureFlagsRequestAttributes represents feature flags request attributes.
type FeatureFlagsRequestAttributes struct {
Flags []string `json:"flags"`
}

// FeatureFlagsResponse represents the updated response format.
type FeatureFlagsResponse struct {
Data FeatureFlagsDataItem `json:"data"`
}

// FeatureFlagsDataItem represents the "data" object (previously slice).
type FeatureFlagsDataItem struct {
ID string `json:"id"`
Type string `json:"type"`
Attributes FeatureFlagAttributesList `json:"attributes"`
}

// FeatureFlagAttributesList represents attributes containing evaluations + evaluated_at.
type FeatureFlagAttributesList struct {
Evaluations []FeatureFlagAttributes `json:"evaluations"`
EvaluatedAt string `json:"evaluatedAt"`
}

// FeatureFlagAttributes represent one evaluation result.
type FeatureFlagAttributes struct {
Key string `json:"key"`
Value bool `json:"value"`
Reason string `json:"reason"`
}
137 changes: 137 additions & 0 deletions cliv2/internal/feature-flag-gateway/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package featureflaggateway

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"time"
)

const (
// ApplicationJSON is the content type for JSON.
ApplicationJSON = "application/json"
userAgent = "User-Agent"
contentType = "Content-Type"
accept = "Accept"
snykCli = "snyk-cli"
authorization = "Authorization"
defaultVersion = "2024-10-15"
)

// Service is a service for interacting with Feature Flag Gateway.
type Service interface {
EvaluateFlags(ctx context.Context, flags []string, orgID string, version string, token string) (*FeatureFlagsResponse, error)
}

// Client is an HTTP client for the Feature Flag Gateway
// endpoint.
type Client struct {
baseURL *url.URL
client *http.Client
}

// NewService creates a new Feature Flag Gateway client.
func NewService(baseURL *url.URL, requestTimeout time.Duration) (*Client, error) {
if baseURL == nil {
return nil, fmt.Errorf("base URL is nil")
}

client := &http.Client{
Timeout: requestTimeout,
}

return &Client{
baseURL: baseURL,
client: client,
}, nil
}

// EvaluateFlags evaluates feature flags for a given organization.
func (c *Client) EvaluateFlags(ctx context.Context, flags []string, orgID string, version string, token string) (featureFlagsResponse *FeatureFlagsResponse, retErr error) {
orgID = strings.TrimSpace(orgID)
if orgID == "" {
return nil, fmt.Errorf("orgID is required")
}

token = strings.TrimSpace(token)
if token == "" {
return nil, fmt.Errorf("token is required")
}

if version == "" {
version = defaultVersion
}

reqBody := buildRequest(flags)
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
return featureFlagsResponse, fmt.Errorf("marshal evaluate flags request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.getBaseURL(orgID, version), bytes.NewReader(bodyBytes))
if err != nil {
return featureFlagsResponse, fmt.Errorf("create evaluate flagsrequest: %w", err)
}

req.Header.Add(contentType, ApplicationJSON)
req.Header.Add(accept, ApplicationJSON)
req.Header.Add(userAgent, snykCli)
req.Header.Set(authorization, "token "+token)

resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("making evaluate flags request: %w", err)
}
defer func() {
retErr = errors.Join(retErr, resp.Body.Close())
}()

if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("evaluate flags failed: status=%d", resp.StatusCode)
return nil, err
}

if err := json.NewDecoder(resp.Body).Decode(&featureFlagsResponse); err != nil {
return nil, fmt.Errorf("decode evaluate flags response: %w", err)
}

return featureFlagsResponse, nil
}

func buildRequest(flags []string) FeatureFlagRequest {
return FeatureFlagRequest{
Data: FeatureFlagsData{
Type: "feature_flags_evaluation",
Attributes: FeatureFlagsRequestAttributes{
Flags: flags,
},
},
}
}

func (c *Client) getBaseURL(orgID, version string) string {
if version == "" {
version = defaultVersion
}

u := *c.baseURL
u.Path = path.Join(
u.Path,
"orgs",
url.PathEscape(orgID),
"feature_flags",
"evaluation",
)

q := u.Query()
q.Set("version", version)
u.RawQuery = q.Encode()

return u.String()
}
Loading