Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dlc purge #879

Merged
merged 4 commits into from
Mar 22, 2023
Merged
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
119 changes: 0 additions & 119 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"strings"

"github.com/CircleCI-Public/circleci-cli/api/graphql"
"github.com/CircleCI-Public/circleci-cli/pipeline"
"github.com/CircleCI-Public/circleci-cli/references"
"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/Masterminds/semver"
Expand Down Expand Up @@ -513,124 +512,6 @@ func WhoamiQuery(cl *graphql.Client) (*WhoamiResponse, error) {
return &response, nil
}

// ConfigQueryLegacy calls the GQL API to validate and process config with the legacy orgSlug
func ConfigQueryLegacy(cl *graphql.Client, configPath string, orgSlug string, params pipeline.Parameters, values pipeline.Values) (*ConfigResponse, error) {
var response BuildConfigResponse
var query string
config, err := loadYaml(configPath)
if err != nil {
return nil, err
}
// GraphQL isn't forwards-compatible, so we are unusually selective here about
// passing only non-empty fields on to the API, to minimize user impact if the
// backend is out of date.
var fieldAddendums string
if orgSlug != "" {
fieldAddendums += ", orgSlug: $orgSlug"
}
if len(params) > 0 {
fieldAddendums += ", pipelineParametersJson: $pipelineParametersJson"
}
query = fmt.Sprintf(
`query ValidateConfig ($config: String!, $pipelineParametersJson: String, $pipelineValues: [StringKeyVal!], $orgSlug: String) {
buildConfig(configYaml: $config, pipelineValues: $pipelineValues%s) {
valid,
errors { message },
sourceYaml,
outputYaml
}
}`,
fieldAddendums)

request := graphql.NewRequest(query)
request.SetToken(cl.Token)
request.Var("config", config)

if values != nil {
request.Var("pipelineValues", pipeline.PrepareForGraphQL(values))
}
if params != nil {
pipelineParameters, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("unable to serialize pipeline values: %s", err.Error())
}
request.Var("pipelineParametersJson", string(pipelineParameters))
}

if orgSlug != "" {
request.Var("orgSlug", orgSlug)
}

err = cl.Run(request, &response)
if err != nil {
return nil, errors.Wrap(err, "Unable to validate config")
}
if len(response.BuildConfig.ConfigResponse.Errors) > 0 {
return nil, &response.BuildConfig.ConfigResponse.Errors
}

return &response.BuildConfig.ConfigResponse, nil
}

// ConfigQuery calls the GQL API to validate and process config with the org id
func ConfigQuery(cl *graphql.Client, configPath string, orgId string, params pipeline.Parameters, values pipeline.Values) (*ConfigResponse, error) {
var response BuildConfigResponse
var query string
config, err := loadYaml(configPath)
if err != nil {
return nil, err
}
// GraphQL isn't forwards-compatible, so we are unusually selective here about
// passing only non-empty fields on to the API, to minimize user impact if the
// backend is out of date.
var fieldAddendums string
if orgId != "" {
fieldAddendums += ", orgId: $orgId"
}
if len(params) > 0 {
fieldAddendums += ", pipelineParametersJson: $pipelineParametersJson"
}
query = fmt.Sprintf(
`query ValidateConfig ($config: String!, $pipelineParametersJson: String, $pipelineValues: [StringKeyVal!], $orgId: UUID!) {
buildConfig(configYaml: $config, pipelineValues: $pipelineValues%s) {
valid,
errors { message },
sourceYaml,
outputYaml
}
}`,
fieldAddendums)

request := graphql.NewRequest(query)
request.SetToken(cl.Token)
request.Var("config", config)

if values != nil {
request.Var("pipelineValues", pipeline.PrepareForGraphQL(values))
}
if params != nil {
pipelineParameters, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("unable to serialize pipeline values: %s", err.Error())
}
request.Var("pipelineParametersJson", string(pipelineParameters))
}

if orgId != "" {
request.Var("orgId", orgId)
}

err = cl.Run(request, &response)
if err != nil {
return nil, errors.Wrap(err, "Unable to validate config")
}
if len(response.BuildConfig.ConfigResponse.Errors) > 0 {
return nil, &response.BuildConfig.ConfigResponse.Errors
}

return &response.BuildConfig.ConfigResponse, nil
}

// OrbQuery validated and processes an orb.
func OrbQuery(cl *graphql.Client, configPath string) (*ConfigResponse, error) {
var response OrbConfigResponse
Expand Down
6 changes: 6 additions & 0 deletions api/dl/dl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dl

// ProjectClient is the interface to interact with dl
type DlClient interface {
PurgeDLC(projectid string) error
}
80 changes: 80 additions & 0 deletions api/dl/dl_rest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package dl

import (
"fmt"
"net/url"
"time"

"github.com/CircleCI-Public/circleci-cli/api/rest"
"github.com/CircleCI-Public/circleci-cli/settings"
)

const defaultDlHost = "https://dl.circleci.com"

type dlRestClient struct {
client *rest.Client
}

// NewDlRestClient returns a new dlRestClient instance initialized with the
// values from the config.
func NewDlRestClient(config settings.Config) (*dlRestClient, error) { //
// We don't want the user to use this with Server as that's nor supported
// at them moment. In order to detect this we look if there's a config file
// or cli option that sets "host" to anything different than the default
if config.Host != "" && config.Host != "https://circleci.com" {
// Only error if there's no custom DlHost set. Since the end user can't
// a custom value set this in the config file, this has to have been
// manually been set in the code, presumably by the test suite to allow
// talking to a mock server, and we want to allow that.
if config.DlHost == "" {
return nil, &CloudOnlyErr{}
}
}

// what's the base URL?
unparsedURL := defaultDlHost
if config.DlHost != "" {
unparsedURL = config.DlHost
}

baseURL, err := url.Parse(unparsedURL)
if err != nil {
return nil, fmt.Errorf("cannot parse dl host URL '%s'", unparsedURL)
}

httpclient := config.HTTPClient
httpclient.Timeout = 10 * time.Second

// the dl endpoint is hardcoded to https://dl.circleci.com, since currently
// this implementation always refers to the cloud dl service
return &dlRestClient{
client: rest.New(
baseURL,
config.Token,
httpclient,
),
}, nil
}

func (c dlRestClient) PurgeDLC(projectid string) error {
// this calls a private circleci endpoint. We make no guarantees about
// this still existing in the future.
path := fmt.Sprintf("private/output/project/%s/dlc", projectid)
req, err := c.client.NewRequest("DELETE", &url.URL{Path: path}, nil)
if err != nil {
return err
}

status, err := c.client.DoRequest(req, nil)

// Futureproofing: If CircleCI ever removes the private backend endpoint
// this call uses, by having the endpoint return a 410 status code CircleCI
// can get everyone running an outdated client to display a helpful error
// telling them to upgrade (presumably by this point a version without this
// logic will have been released)
if status == 410 {
return &GoneErr{}
}

return err
}
81 changes: 81 additions & 0 deletions api/dl/dl_rest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package dl

import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"

"gotest.tools/v3/assert"

"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/CircleCI-Public/circleci-cli/version"
)

// getDlRestClient returns a dlRestClient hooked up to the passed server
func getDlRestClient(server *httptest.Server) (*dlRestClient, error) {
return NewDlRestClient(settings.Config{
DlHost: server.URL,
HTTPClient: http.DefaultClient,
Token: "token",
})
}

func Test_DLCPurge(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
wantErr bool
}{
{
name: "Should handle a successful request",
handler: func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Header.Get("circle-token"), "token")
assert.Equal(t, r.Header.Get("accept"), "application/json")
assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent())

assert.Equal(t, r.Method, "DELETE")
assert.Equal(t, r.URL.Path, fmt.Sprintf("/private/output/project/%s/dlc", "projectid"))

// check the request was made with an empty body
br := r.Body
b, err := io.ReadAll(br)
assert.NilError(t, err)
assert.Equal(t, string(b), "")
assert.NilError(t, br.Close())

// send response as empty 200
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(``))
assert.NilError(t, err)
},
},
{
name: "Should handle an error request",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_, err := w.Write([]byte(`{"message": "error"}`))
assert.NilError(t, err)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(tt.handler)
defer server.Close()

c, err := getDlRestClient(server)
assert.NilError(t, err)

err = c.PurgeDLC("projectid")
if (err != nil) != tt.wantErr {
t.Errorf("PurgeDLC() error = %#v (%s), wantErr %v", err, err, tt.wantErr)
return
}
})
}
}
27 changes: 27 additions & 0 deletions api/dl/err.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dl

type CloudOnlyErr struct{}

func (e *CloudOnlyErr) Error() string {
return "Misconfiguration.\n" +
"You have configured a custom API endpoint host for the circleci CLI.\n" +
"However, this functionality is only supported on circleci.com API endpoints."
}

func IsCloudOnlyErr(err error) bool {
_, ok := err.(*CloudOnlyErr)
return ok
}

type GoneErr struct{}

func (e *GoneErr) Error() string {
return "No longer supported.\n" +
"This functionality is no longer supported by this version of the circleci CLI.\n" +
"Please upgrade to the latest version of the circleci CLI."
}

func IsGoneErr(err error) bool {
_, ok := err.(*GoneErr)
return ok
}
6 changes: 6 additions & 0 deletions api/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ type ProjectEnvironmentVariable struct {
Value string
}

// ProjectInfo is the info of a Project
type ProjectInfo struct {
Id string
}

// ProjectClient is the interface to interact with project and it's
// components.
type ProjectClient interface {
ProjectInfo(vcs, org, project string) (*ProjectInfo, error)
ListAllEnvironmentVariables(vcs, org, project string) ([]*ProjectEnvironmentVariable, error)
GetEnvironmentVariable(vcs, org, project, envName string) (*ProjectEnvironmentVariable, error)
CreateEnvironmentVariable(vcs, org, project string, v ProjectEnvironmentVariable) (*ProjectEnvironmentVariable, error)
Expand Down
Loading