Skip to content

Commit

Permalink
feat: [CI-11277]: Support Signed URLs for Cache Intelligence workflows (
Browse files Browse the repository at this point in the history
#93)

* fix: [CI-11277]: Support Signed URLs for Cache Intelligence workflows

* fix

* fix
  • Loading branch information
sahithibanda01 authored Mar 19, 2024
1 parent 0a9466e commit c6fcb41
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 0 deletions.
25 changes: 25 additions & 0 deletions harness/client.go
Original file line number Diff line number Diff line change
@@ -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)
}
88 changes: 88 additions & 0 deletions harness/http.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions internal/plugin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -41,4 +42,5 @@ type Config struct {
SFTP sftp.Config
Azure azure.Config
GCS gcs.Config
Harness harness.Config
}
1 change: 1 addition & 0 deletions internal/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"),
}
Expand Down
6 changes: 6 additions & 0 deletions storage/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions storage/backend/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -17,4 +18,5 @@ type Config struct {
SFTP sftp.Config
Azure azure.Config
GCS gcs.Config
Harness harness.Config
}
8 changes: 8 additions & 0 deletions storage/backend/harness/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package harness

// Config is a structure to store harness backend configuration.
type Config struct {
AccountID string
Token string
ServerBaseURL string
}
117 changes: 117 additions & 0 deletions storage/backend/harness/harness.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit c6fcb41

Please sign in to comment.