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

Allow to create components in resources folder to kubernetes #1473

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ require (
k8s.io/kubectl v0.26.0 // indirect
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
oras.land/oras-go v1.2.2 // indirect
sigs.k8s.io/controller-runtime v0.16.3 // indirect
sigs.k8s.io/controller-runtime v0.16.3
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.15.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.15.0 // indirect
Expand Down
49 changes: 49 additions & 0 deletions pkg/kubernetes/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@
package kubernetes

import (
"errors"
"flag"
"sync"

"k8s.io/apimachinery/pkg/runtime"
k8s "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"

scheme "github.com/dapr/dapr/pkg/client/clientset/versioned"

Expand All @@ -31,6 +34,14 @@

// oidc auth
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"

Check failure on line 37 in pkg/kubernetes/client.go

View workflow job for this annotation

GitHub Actions / Build linux_amd64 binaries

File is not `goimports`-ed with -local github.com/dapr/ (goimports)
componentsapi "github.com/dapr/dapr/pkg/apis/components/v1alpha1"
configurationapi "github.com/dapr/dapr/pkg/apis/configuration/v1alpha1"
httpendpointsapi "github.com/dapr/dapr/pkg/apis/httpEndpoint/v1alpha1"
resiliencyapi "github.com/dapr/dapr/pkg/apis/resiliency/v1alpha1"
subscriptionsapiV1alpha1 "github.com/dapr/dapr/pkg/apis/subscriptions/v1alpha1"
subapi "github.com/dapr/dapr/pkg/apis/subscriptions/v2alpha1"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
)

var (
Expand Down Expand Up @@ -96,3 +107,41 @@
}
return scheme.NewForConfig(config)
}

// buildScheme builds the scheme for the controller-runtime client
// from https://github.com/dapr/dapr/blob/eb49e564fbd704ceb1379498fc8e94ad7110840f/pkg/operator/operator.go#L444-L466
func buildScheme() (*runtime.Scheme, error) {
builders := []func(*runtime.Scheme) error{
clientgoscheme.AddToScheme,
componentsapi.AddToScheme,
configurationapi.AddToScheme,
resiliencyapi.AddToScheme,
httpendpointsapi.AddToScheme,
subscriptionsapiV1alpha1.AddToScheme,
subapi.AddToScheme,
}

errs := make([]error, len(builders))
scheme := runtime.NewScheme()
for i, builder := range builders {
errs[i] = builder(scheme)
}

return scheme, errors.Join(errs...)
}

// CtrlClient returns a new Controller-Runtime Client (https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/client) - no caching
// with the scheme built with the Dapr API groups.
func CtrlClient() (client.Client, error) {
config, err := getConfig()
if err != nil {
return nil, err
}

scheme, err := buildScheme()
if err != nil {
return nil, err
}

return client.New(config, client.Options{Scheme: scheme})
}
91 changes: 91 additions & 0 deletions pkg/kubernetes/resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package kubernetes

import (
"context"
"fmt"
"os"
"path/filepath"
"strings"

k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/dapr/cli/pkg/print"
)

// getResources returns a list of Kubernetes resources from the given resources folder.
// There can be only one configuration file as dapr cli is not enabled to take a configuration per app and can only
// use the configuration named `appconfig`.
func getResources(resourcesFolder string) ([]client.Object, error) {
// Create YAML decoder

Check failure on line 22 in pkg/kubernetes/resources.go

View workflow job for this annotation

GitHub Actions / Build linux_amd64 binaries

Comment should end in a period (godot)
decUnstructured := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)

// Read files from the resources folder

Check failure on line 25 in pkg/kubernetes/resources.go

View workflow job for this annotation

GitHub Actions / Build linux_amd64 binaries

Comment should end in a period (godot)
files, err := os.ReadDir(resourcesFolder)
if err != nil {
return nil, fmt.Errorf("error reading resources folder: %w", err)
}

// we can only have one configuration file as dapr cli is not enabled to take a configuration per app and can only
// use the configuration named `appconfig`.
var configurationAlreadyFound bool

var resources []client.Object

Check failure on line 35 in pkg/kubernetes/resources.go

View workflow job for this annotation

GitHub Actions / Build linux_amd64 binaries

Consider pre-allocating `resources` (prealloc)
for _, file := range files {
if file.IsDir() || (!strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".json")) {
continue
}

// Read file content

Check failure on line 41 in pkg/kubernetes/resources.go

View workflow job for this annotation

GitHub Actions / Build linux_amd64 binaries

Comment should end in a period (godot)
content, err := os.ReadFile(filepath.Join(resourcesFolder, file.Name()))
if err != nil {
return nil, fmt.Errorf("error reading file %s: %w", file.Name(), err)
}

// Decode YAML/JSON to unstructured
obj := &unstructured.Unstructured{}
_, _, err = decUnstructured.Decode(content, nil, obj)
if err != nil {
return nil, fmt.Errorf("error decoding file %s: %w", file.Name(), err)
}

// check if the resource is a configuration
if obj.GetKind() == "Configuration" {
if configurationAlreadyFound {
return nil, fmt.Errorf("error: multiple configuration files found in %s. Only one configuration file is allowed", resourcesFolder)
}
configurationAlreadyFound = true
obj.SetName("appconfig")
}

resources = append(resources, obj)
}

return resources, nil
}

func createOrUpdateResources(ctx context.Context, cl client.Client, resources []client.Object, namespace string) error {
// create resources in k8s
for _, resource := range resources {
// clone the resource to avoid modifying the original
obj := resource.DeepCopyObject().(*unstructured.Unstructured)
// Set namespace on the resource metadata
obj.SetNamespace(namespace)

print.InfoStatusEvent(os.Stdout, "Deploying resource %q kind %q to Kubernetes", obj.GetName(), obj.GetKind())

if err := cl.Create(ctx, obj); err != nil {
if k8serrors.IsAlreadyExists(err) {
print.InfoStatusEvent(os.Stdout, "Resource %q kind %q already exists, updating", obj.GetName(), obj.GetKind())
if err := cl.Update(ctx, obj); err != nil {

Check failure on line 82 in pkg/kubernetes/resources.go

View workflow job for this annotation

GitHub Actions / Build linux_amd64 binaries

shadow: declaration of "err" shadows declaration at line 79 (govet)
return err
}
} else {
return fmt.Errorf("error deploying resource %q kind %q to Kubernetes: %w", obj.GetName(), obj.GetKind(), err)
}
}
}
return nil
}
50 changes: 50 additions & 0 deletions pkg/kubernetes/resources_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package kubernetes

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetResources(t *testing.T) {
tests := []struct {
name string
folder string
expectError bool
expectedCount int
expectedResourceKinds []string
}{
{
name: "resources from testdata",
folder: filepath.Join("testdata", "resources"),
expectError: false,
expectedCount: 3,
expectedResourceKinds: []string{"Component", "Configuration", "Resiliency"},
},
{
name: "non-existent folder",
folder: filepath.Join("testdata", "non-existent"),
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resources, err := getResources(tt.folder)
if tt.expectError {
assert.Error(t, err)
return
}

require.NoError(t, err)
assert.Len(t, resources, tt.expectedCount)
foundKinds := []string{}
for _, resource := range resources {
foundKinds = append(foundKinds, resource.GetObjectKind().GroupVersionKind().Kind)
}
assert.ElementsMatch(t, tt.expectedResourceKinds, foundKinds)
})
}
}
23 changes: 18 additions & 5 deletions pkg/kubernetes/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ func Run(runFilePath string, config runfileconfig.RunFileConfig) (bool, error) {
runStates := []runState{}
print.InfoStatusEvent(os.Stdout, "This is a preview feature and subject to change in future releases.")

ctrlClient, cErr := CtrlClient()
if cErr != nil {
print.FailureStatusEvent(os.Stderr, "Error getting controller-runtime k8s client: %s", cErr.Error())
exitWithError = true
}

resources, err := getResources(config.Common.ResourcesPath)
if err != nil {
print.FailureStatusEvent(os.Stderr, "Error getting resources from %q: %s", config.Common.ResourcesPath, err.Error())
exitWithError = true
}

if err := createOrUpdateResources(context.Background(), ctrlClient, resources, namespace); err != nil {
print.FailureStatusEvent(os.Stderr, "Error creating or updating resources: %s", err.Error())
exitWithError = true
}

for _, app := range config.Apps {
print.StatusEvent(os.Stdout, print.LogInfo, "Validating config and starting app %q", app.RunConfig.AppID)
// Set defaults if zero value provided in config yaml.
Expand All @@ -140,11 +157,7 @@ func Run(runFilePath string, config runfileconfig.RunFileConfig) (bool, error) {

// create default deployment config.
dep := createDeploymentConfig(daprClient, app)
if err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this error is always nil at this point

print.FailureStatusEvent(os.Stderr, "Error creating deployment file for app %q present in %s: %s", app.RunConfig.AppID, runFilePath, err.Error())
exitWithError = true
break
}

// overwrite <app-id>/.dapr/deploy/service.yaml.
// overwrite <app-id>/.dapr/deploy/deployment.yaml.

Expand Down
10 changes: 10 additions & 0 deletions pkg/kubernetes/testdata/resources/observability.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: daprConfig
namespace: default
spec:
tracing:
samplingRate: "1"
zipkin:
endpointAddress: "http://localhost:9411/api/v2/spans"
26 changes: 26 additions & 0 deletions pkg/kubernetes/testdata/resources/resiliency.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apiVersion: dapr.io/v1alpha1
kind: Resiliency
metadata:
name: myresiliency
scopes:
- checkout

spec:
policies:
retries:
retryForever:
policy: constant
duration: 5s
maxRetries: -1

circuitBreakers:
simpleCB:
maxRequests: 1
timeout: 5s
trip: consecutiveFailures >= 5

targets:
apps:
order-processor:
retry: retryForever
circuitBreaker: simpleCB
15 changes: 15 additions & 0 deletions pkg/kubernetes/testdata/resources/state_redis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
initTimeout: 1m
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"
Loading