diff --git a/harness/client.go b/harness/client.go new file mode 100644 index 00000000..dc30f341 --- /dev/null +++ b/harness/client.go @@ -0,0 +1,25 @@ +package harness + +import ( + "context" + "fmt" +) + +// Error is a custom error struct +type Error struct { + Code int + Message string +} + +func (e *Error) Error() string { + return fmt.Sprintf("%d: %s", e.Code, e.Message) +} + +// Client defines a cache service client. +type Client interface { + GetUploadURL(ctx context.Context, key string) (string, error) + + GetDownloadURL(ctx context.Context, key string) (string, error) + + GetExistsURL(ctx context.Context, key string) (string, error) +} diff --git a/harness/http.go b/harness/http.go new file mode 100644 index 00000000..679b53cc --- /dev/null +++ b/harness/http.go @@ -0,0 +1,88 @@ +package harness + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" +) + +var _ Client = (*HTTPClient)(nil) + +const ( + RestoreEndpoint = "/cache/intel/download?accountId=%s&cacheKey=%s" + StoreEndpoint = "/cache/intel/upload?accountId=%s&cacheKey=%s" + ExistsEndpoint = "/cache/intel/exists?accountId=%s&cacheKey=%s" +) + +// NewHTTPClient returns a new HTTPClient. +func New(endpoint, accountID, bearerToken string, skipverify bool) *HTTPClient { + endpoint = strings.TrimSuffix(endpoint, "/") + client := &HTTPClient{ + Endpoint: endpoint, + BearerToken: bearerToken, + AccountID: accountID, + Client: &http.Client{ + CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + } + return client +} + +// HTTPClient provides an http service client. +type HTTPClient struct { + Client *http.Client + Endpoint string + AccountID string + BearerToken string +} + +// getUploadURL will get the 'put' presigned url from cache service +func (c *HTTPClient) GetUploadURL(ctx context.Context, key string) (string, error) { + path := fmt.Sprintf(StoreEndpoint, c.AccountID, key) + return c.getLink(ctx, c.Endpoint+path) +} + +// getDownloadURL will get the 'get' presigned url from cache service +func (c *HTTPClient) GetDownloadURL(ctx context.Context, key string) (string, error) { + path := fmt.Sprintf(RestoreEndpoint, c.AccountID, key) + return c.getLink(ctx, c.Endpoint+path) +} + +// getExistsURL will get the 'exists' presigned url from cache service +func (c *HTTPClient) GetExistsURL(ctx context.Context, key string) (string, error) { + path := fmt.Sprintf(ExistsEndpoint, c.AccountID, key) + return c.getLink(ctx, c.Endpoint+path) +} + +func (c *HTTPClient) getLink(ctx context.Context, path string) (string, error) { + req, err := http.NewRequestWithContext(ctx, "GET", path, nil) + if err != nil { + return "", err + } + if c.BearerToken != "" { + req.Header.Add("X-Harness-Token", c.BearerToken) + } + + resp, err := c.client().Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to get link with status %d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(body), nil +} + +func (c *HTTPClient) client() *http.Client { + return c.Client +} diff --git a/internal/plugin/config.go b/internal/plugin/config.go index 500eda6a..79188e42 100644 --- a/internal/plugin/config.go +++ b/internal/plugin/config.go @@ -6,6 +6,7 @@ import ( "github.com/meltwater/drone-cache/storage/backend/azure" "github.com/meltwater/drone-cache/storage/backend/filesystem" "github.com/meltwater/drone-cache/storage/backend/gcs" + "github.com/meltwater/drone-cache/storage/backend/harness" "github.com/meltwater/drone-cache/storage/backend/s3" "github.com/meltwater/drone-cache/storage/backend/sftp" ) @@ -41,4 +42,5 @@ type Config struct { SFTP sftp.Config Azure azure.Config GCS gcs.Config + Harness harness.Config } diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index f0c76259..119c09e7 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -146,6 +146,7 @@ func (p *Plugin) Exec() error { // nolint:funlen GCS: cfg.GCS, S3: cfg.S3, SFTP: cfg.SFTP, + Harness: cfg.Harness, }) if err != nil { return fmt.Errorf("initialize backend <%s>, %w", cfg.Backend, err) diff --git a/main.go b/main.go index 8af4f685..09952a29 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "github.com/meltwater/drone-cache/storage/backend/azure" "github.com/meltwater/drone-cache/storage/backend/filesystem" "github.com/meltwater/drone-cache/storage/backend/gcs" + "github.com/meltwater/drone-cache/storage/backend/harness" "github.com/meltwater/drone-cache/storage/backend/s3" "github.com/meltwater/drone-cache/storage/backend/sftp" @@ -504,6 +505,16 @@ func main() { Usage: "sftp port", EnvVars: []string{"SFTP_PORT"}, }, + &cli.StringFlag{ + Name: "cache-service-token", + Usage: "cache service token", + EnvVars: []string{"PLUGIN_CACHE_SERVICE_BEARER_TOKEN"}, + }, + &cli.StringFlag{ + Name: "cache-service-baseurl", + Usage: "cache service base url", + EnvVars: []string{"PLUGIN_CACHE_SERVICE_BASE_URL"}, + }, } if err := app.Run(os.Args); err != nil { @@ -620,6 +631,11 @@ func run(c *cli.Context) error { Encryption: c.String("gcs.encryption-key"), Timeout: c.Duration("backend.operation-timeout"), }, + Harness: harness.Config{ + AccountID: c.String("account-id"), + Token: c.String("cache-service-token"), + ServerBaseURL: c.String("cache-service-baseurl"), + }, SkipSymlinks: c.Bool("skip-symlinks"), } diff --git a/storage/backend/backend.go b/storage/backend/backend.go index 583a26dd..650425b8 100644 --- a/storage/backend/backend.go +++ b/storage/backend/backend.go @@ -12,6 +12,7 @@ import ( "github.com/meltwater/drone-cache/storage/backend/azure" "github.com/meltwater/drone-cache/storage/backend/filesystem" "github.com/meltwater/drone-cache/storage/backend/gcs" + "github.com/meltwater/drone-cache/storage/backend/harness" "github.com/meltwater/drone-cache/storage/backend/s3" "github.com/meltwater/drone-cache/storage/backend/sftp" "github.com/meltwater/drone-cache/storage/common" @@ -28,6 +29,8 @@ const ( S3 = "s3" // SFTP type of the corresponding backend represented as string constant. SFTP = "sftp" + //Harness type of the corresponding backend represented as string constant. + Harness = "harness" ) // Backend implements operations for caching files. @@ -62,6 +65,9 @@ func FromConfig(l log.Logger, backedType string, cfg Config) (Backend, error) { case S3: level.Debug(l).Log("msg", "using aws s3 as backend") b, err = s3.New(log.With(l, "backend", S3), cfg.S3, cfg.Debug) + case Harness: + level.Debug(l).Log("msg", "using harness as backend") + b, err = harness.New(log.With(l, "backend", Harness), cfg.Harness, cfg.Debug) case GCS: level.Debug(l).Log("msg", "using gc storage as backend") b, err = gcs.New(log.With(l, "backend", GCS), cfg.GCS) diff --git a/storage/backend/config.go b/storage/backend/config.go index ef40b4c6..c534cc27 100644 --- a/storage/backend/config.go +++ b/storage/backend/config.go @@ -4,6 +4,7 @@ import ( "github.com/meltwater/drone-cache/storage/backend/azure" "github.com/meltwater/drone-cache/storage/backend/filesystem" "github.com/meltwater/drone-cache/storage/backend/gcs" + "github.com/meltwater/drone-cache/storage/backend/harness" "github.com/meltwater/drone-cache/storage/backend/s3" "github.com/meltwater/drone-cache/storage/backend/sftp" ) @@ -17,4 +18,5 @@ type Config struct { SFTP sftp.Config Azure azure.Config GCS gcs.Config + Harness harness.Config } diff --git a/storage/backend/harness/config.go b/storage/backend/harness/config.go new file mode 100644 index 00000000..da851de0 --- /dev/null +++ b/storage/backend/harness/config.go @@ -0,0 +1,8 @@ +package harness + +// Config is a structure to store harness backend configuration. +type Config struct { + AccountID string + Token string + ServerBaseURL string +} diff --git a/storage/backend/harness/harness.go b/storage/backend/harness/harness.go new file mode 100644 index 00000000..52840ffc --- /dev/null +++ b/storage/backend/harness/harness.go @@ -0,0 +1,117 @@ +package harness + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + + "github.com/go-kit/kit/log" + "github.com/meltwater/drone-cache/harness" + "github.com/meltwater/drone-cache/internal" + "github.com/meltwater/drone-cache/storage/common" +) + +type Backend struct { + logger log.Logger + token string + client harness.Client +} + +// New creates an Harness backend. +func New(l log.Logger, c Config, debug bool) (*Backend, error) { + cacheClient := harness.New(c.ServerBaseURL, c.AccountID, c.Token, false) + backend := &Backend{ + logger: l, + token: c.Token, + client: cacheClient, + } + return backend, nil +} + +func (b *Backend) Get(ctx context.Context, key string, w io.Writer) error { + preSignedURL, err := b.client.GetDownloadURL(ctx, key) + if err != nil { + return err + } + res, err := b.do(ctx, "GET", preSignedURL, nil) + if err != nil { + return err + } + defer internal.CloseWithErrLogf(b.logger, res.Body, "response body, close defer") + if res.StatusCode != http.StatusOK { + return fmt.Errorf("received status code %d from presigned get url", res.StatusCode) + } + _, err = io.Copy(w, res.Body) + if err != nil { + return err + } + + return nil +} + +func (b *Backend) Put(ctx context.Context, key string, r io.Reader) error { + preSignedURL, err := b.client.GetUploadURL(ctx, key) + if err != nil { + return err + } + res, err := b.do(ctx, "PUT", preSignedURL, r) + if err != nil { + return err + } + defer internal.CloseWithErrLogf(b.logger, res.Body, "response body, close defer") + if res.StatusCode != http.StatusOK { + return fmt.Errorf("received status code %d from presigned put url", res.StatusCode) + } + + return nil +} + +func (b *Backend) Exists(ctx context.Context, key string) (bool, error) { + preSignedURL, err := b.client.GetExistsURL(ctx, key) + if err != nil { + return false, err + } + res, err := b.do(ctx, "HEAD", preSignedURL, nil) + if err != nil { + return false, nil + } + defer internal.CloseWithErrLogf(b.logger, res.Body, "response body, close defer") + if res.StatusCode == http.StatusNotFound { + return false, nil + } else if res.StatusCode != http.StatusOK { + return false, fmt.Errorf("unexpected status code %d", res.StatusCode) + } + + return res.Header.Get("ETag") != "", nil +} + +func (b *Backend) List(ctx context.Context, key string) ([]common.FileEntry, error) { + // not implemented + return nil, errors.New("list operation not implemented") +} + +func (b *Backend) do(ctx context.Context, method, url string, body io.Reader) (*http.Response, error) { + var ( + buffer []byte + err error + ) + if body != nil { + buffer, err = io.ReadAll(body) + if err != nil { + return nil, err + } + } + req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(buffer)) + if err != nil { + return nil, err + } + httpClient := http.Client{} + res, err := httpClient.Do(req) + if err != nil { + return nil, err + } + return res, nil +}