Skip to content

Commit

Permalink
feat(provisioner): provisioner server implementation (#18)
Browse files Browse the repository at this point in the history
* test(provisioner): prepare base unit tests

Signed-off-by: Mateusz Urbanek <[email protected]>

* feat(provisioner): added server side calls

Signed-off-by: Mateusz Urbanek <[email protected]>

* test(provisioner): fixed and extended units

Signed-off-by: Mateusz Urbanek <[email protected]>

* fix(examples): set correct cluster in the example

Signed-off-by: Mateusz Urbanek <[email protected]>

* test(provisioner): added integration tests

Signed-off-by: Mateusz Urbanek <[email protected]>

* docs(readme): added basic usage

Signed-off-by: Mateusz Urbanek <[email protected]>

* fix(review/1): include error in the error log

Signed-off-by: Mateusz Urbanek <[email protected]>

* fix(review/2): check for span nillnes

Signed-off-by: Mateusz Urbanek <[email protected]>

* fix(review/2): invert logic in CreateBucket

Signed-off-by: Mateusz Urbanek <[email protected]>

---------

Signed-off-by: Mateusz Urbanek <[email protected]>
  • Loading branch information
shanduur-akamai authored Jan 13, 2024
1 parent 0042442 commit de570f7
Show file tree
Hide file tree
Showing 17 changed files with 948 additions and 75 deletions.
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ run:
# Include test files.
tests: true

# List of build tags to pass to all linters.
build-tags:
- integration

issues:
# Set to 0 to not skip any issues.
max-issues-per-linter: 0
Expand Down
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ The Linode COSI Driver is an implementation of the Kubernetes Container Object S

- [Linode COSI Driver](#linode-cosi-driver)
- [Getting Started](#getting-started)
- [Testing](#testing)
- [Integration tests](#integration-tests)
- [Prerequisites](#prerequisites)
- [Test Execution](#test-execution)
- [Configuration](#configuration)
- [Test Cases](#test-cases)
- [Happy Path Test](#happy-path-test)
- [Suite Structure](#suite-structure)
- [License](#license)
- [Support](#support)
- [Contributing](#contributing)
Expand Down Expand Up @@ -41,7 +49,84 @@ Follow these steps to get started with Linode COSI Driver:
```

3. **Usage:**
<!-- TODO: write usage examples -->
1. Create Bucket Class (see the [example.BucketClass.yaml](./examples/example.BucketClass.yaml)).
```sh
kubectl create -f ./examples/example.BucketClass.yaml
```

2. Create Bucket Access Class (see the [example.BucketAccessClass.yaml](./examples/example.BucketAccessClass.yaml)).
```sh
kubectl create -f ./examples/example.BucketAccessClass.yaml
```

3. Create Bucket Claim (see the [example.BucketClaim.yaml](./examples/example.BucketClaim.yaml)).
```sh
kubectl create -f ./examples/example.BucketClaim.yaml
```

4. Create Bucket Access Class (see the [example.BucketAccess.yaml](./examples/example.BucketAccess.yaml)).
```sh
kubectl create -f ./examples/example.BucketAccess.yaml
```

5. Use the `example-secret` secret in your workload, e.g. in deployment:
```yaml
spec:
template:
spec:
containers:
- volumeMounts:
- mountPath: /conf
name: BucketInfo
volumes:
- name: example-secret-vol
secret:
name: example-secret
items:
- key: BucketInfo
path: BucketInfo.json
```

## Testing

### Integration tests

#### Prerequisites

Before running the integration tests, ensure the following prerequisites are met:

- **Linode Account**: You need a valid Linode account with access to the Linode API.
- **Linode Token**: Set the `LINODE_TOKEN` environment variable with your Linode API token.
- **Environment Variables**: Additional environment variables, such as `LINODE_API_URL` and `LINODE_API_VERSION`, can be set as needed.

#### Test Execution

To run the integration tests, execute the following:

```bash
go test -tags=integration ./...
```

The tests cover various operations such as creating a bucket, granting and revoking bucket access, and deleting a bucket. These operations are performed multiple times to ensure idempotency.

#### Configuration

The test suite provides configurable parameters through environment variables:

- `LINODE_TOKEN`: Linode API token.
- `LINODE_API_URL`: Linode API URL.
- `LINODE_API_VERSION`: Linode API version.
- `IDEMPOTENCY_ITERATIONS`: Number of times to run idempotent operations (default is 2).

#### Test Cases

##### Happy Path Test

The `TestHappyPath` function executes a series of idempotent operations on the Linode COSI driver, covering bucket creation, access granting and revoking, and bucket deletion. The test validates the driver's functionality under normal conditions.
#### Suite Structure
The test suite is organized into a `suite` struct, providing a clean separation of concerns for different test operations. The suite includes methods for creating, deleting, granting access to, and revoking access from a bucket. These methods are called in an idempotent loop to ensure the driver's robustness.

## License

Expand Down
3 changes: 2 additions & 1 deletion examples/example.BucketAccessClass.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ metadata:
driverName: objectstorage.cosi.linode.com
authenticationType: Key
parameters:
permissions: Full # ReadOnly
cosi.linode.com/v1/permissions: read_only

4 changes: 3 additions & 1 deletion examples/example.BucketClass.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ metadata:
driverName: objectstorage.cosi.linode.com
deletionPolicy: Delete
parameters:
region: us-east
cosi.linode.com/v1/region: us-east-1
cosi.linode.com/v1/acl: private
cosi.linode.com/v1/cors: disabled
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0
go.opentelemetry.io/otel/metric v1.21.0
go.opentelemetry.io/otel/sdk v1.21.0
go.opentelemetry.io/otel/sdk/metric v1.21.0
go.opentelemetry.io/otel/trace v1.21.0
Expand All @@ -26,7 +27,6 @@ require (
github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions helm/linode-cosi-driver/templates/Secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ metadata:
type: Opaque
data:
LINODE_TOKEN: {{ required "value 'apiToken' required" .Values.apiToken | b64enc }}
{{- if .Values.linodeApiUrl -}}
{{- if .Values.linodeApiUrl }}
LINODE_API_URL: {{ .Values.linodeApiUrl | b64enc }}
{{- end }}
{{- if .Values.linodeApiVersion -}}
{{- if .Values.linodeApiVersion }}
LINODE_API_VERSION: {{ .Values.linodeApiVersion | b64enc }}
{{- end }}
{{- end }}
13 changes: 13 additions & 0 deletions pkg/envflag/envflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,16 @@ func Bool(envKey string, defaultValue bool) bool {

return defaultValue
}

func Int(envKey string, defaultValue int) int {
val, ok := os.LookupEnv(envKey)
if !ok {
return defaultValue
}

if actual, err := strconv.Atoi(val); err == nil {
return actual
}

return defaultValue
}
48 changes: 48 additions & 0 deletions pkg/envflag/envflag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,51 @@ func TestBool(t *testing.T) {
})
}
}

//nolint:paralleltest
func TestInts(t *testing.T) {
const (
DefaultValue = 1
Key = "KEY"
Value = 10
)

for _, tc := range []struct {
name string // required
key string
value int
defaultValue int
expectedValue int
}{
{
name: "simple",
},
{
name: "with default value",
defaultValue: DefaultValue,
expectedValue: DefaultValue,
},
{
name: "with actual value",
key: Key,
value: Value,
defaultValue: DefaultValue,
expectedValue: Value,
},
} {
tc := tc

t.Run(tc.name, func(t *testing.T) {
if tc.key != "" {
tc.key = fmt.Sprintf("TEST_%d_%s", rand.Intn(256), tc.key) // #nosec G404

t.Setenv(tc.key, fmt.Sprint(tc.value))
}

actual := envflag.Int(tc.key, tc.defaultValue)
if actual != tc.expectedValue {
t.Errorf("expected: %d, got: %d", tc.expectedValue, actual)
}
})
}
}
7 changes: 7 additions & 0 deletions pkg/observability/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
)

const meterName = "github.com/linode/linode-cosi-driver/pkg/observability/metrics"

func Setup(ctx context.Context, resource *resource.Resource, protocol string) (_ func(context.Context) error, err error) {
var exp sdkmetric.Exporter

Expand Down Expand Up @@ -64,3 +67,7 @@ func registerMetricsExporter(res *resource.Resource, exporter sdkmetric.Exporter

return mp.Shutdown, nil
}

func Meter() metric.Meter {
return otel.Meter(meterName)
}
30 changes: 30 additions & 0 deletions pkg/observability/tracing/tracing.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@ import (

o11y "github.com/linode/linode-cosi-driver/pkg/observability"
"go.opentelemetry.io/otel"
otelcodes "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
grpccodes "google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

const tracerName = "github.com/linode/linode-cosi-driver/pkg/observability/tracing"

func Setup(ctx context.Context, resource *resource.Resource, protocol string) (_ func(context.Context) error, err error) {
var exp sdktrace.SpanExporter

Expand Down Expand Up @@ -65,3 +71,27 @@ func registerTraceExporter(res *resource.Resource, exporter sdktrace.SpanExporte
// Shutdown will flush any remaining spans and shut down the exporter.
return tp.Shutdown, nil
}

func Start(ctx context.Context, name string) (context.Context, trace.Span) {
return otel.Tracer(tracerName).Start(ctx, name)
}

// Error returns an error representing code and error message and records new event on the span.
// If code is OK, returns nil.
func Error(span trace.Span, code grpccodes.Code, err error, events ...string) error {
if span != nil {
for _, event := range events {
span.AddEvent(event)
}
}

if err != nil && span != nil {
span.RecordError(err)

if code != grpccodes.OK {
span.SetStatus(otelcodes.Error, err.Error())
}
}

return status.Error(code, fmt.Sprintf("%v", err))
}
8 changes: 2 additions & 6 deletions pkg/servers/identity/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@

package identity

import (
"go.opentelemetry.io/otel"
)

const meterName = "linode/linode-cosi-driver/servers/identity"
import "github.com/linode/linode-cosi-driver/pkg/observability/metrics"

// registerMetrics is the common place of registering new metrics to the server.
// When creating new metrics from the meter1, we call something like:
Expand All @@ -27,7 +23,7 @@ const meterName = "linode/linode-cosi-driver/servers/identity"
//
// As we expect the metrics to be registered, it is important to return and handle the error.
func (s *Server) registerMetrics() error {
_ = otel.Meter(meterName)
_ = metrics.Meter()

// TODO: any new metrics should be placed here.

Expand Down
82 changes: 82 additions & 0 deletions pkg/servers/provisioner/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2023 Akamai Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package provisioner

import (
"errors"
"net/http"

"github.com/linode/linodego"
)

const (
ParamRegion = "cosi.linode.com/v1/region"
ParamACL = "cosi.linode.com/v1/acl"
ParamCORS = "cosi.linode.com/v1/cors"
ParamPermissions = "cosi.linode.com/v1/permissions"
)

type ParamCORSValue string

const (
ParamCORSValueEnabled ParamCORSValue = "enabled"
ParamCORSValueDisabled ParamCORSValue = "disabled"
)

func (v ParamCORSValue) Bool() bool {
return v == ParamCORSValueEnabled
}

func (v ParamCORSValue) BoolP() *bool {
p := v == ParamCORSValueEnabled
return &p
}

type ParamPermissionsValue string

const (
ParamPermissionsValueReadOnly ParamPermissionsValue = "read_only"
ParamPermissionsValueReadWrite ParamPermissionsValue = "read_write"
)

const (
S3 = "s3"
S3Region = "region"
S3Endpoint = "endpoint"
S3SecretAccessKeyID = "accessKeyID"
S3SecretAccessSecretKey = "accessSecretKey"
)

var (
ErrNotFound = linodego.Error{Code: http.StatusNotFound}
ErrBucketExists = errors.New("bucket exists with different parameters")
ErrUnsuportedAuth = errors.New("unsupported authentication schema")
ErrMissingRegion = errors.New("region was not provided")
ErrUnknownPermsissions = errors.New("unknown permissions")
)

const (
KeyBucketID = "bucket.id"
KeyBucketLabel = "bucket.label"
KeyBucketCluster = "bucket.cluster"
KeyBucketCreationTimestamp = "bucket.created_at"
KeyBucketACL = "bucket.acl"
KeyBucketCORS = "bucket.cors_enabled"
KeyBucketAccessIDRaw = "bucket.access.id_raw"
KeyBucketAccessID = "bucket.access.id"
KeyBucketAccessName = "bucket.access.name"
KeyBucketAccessAuth = "bucket.access.auth"
KeyBucketAccessPermissions = "bucket.access.permissions"
)
Loading

0 comments on commit de570f7

Please sign in to comment.