Skip to content

Commit

Permalink
Merge pull request #440 from LF-Decentralized-Trust-labs/palaidn-secret
Browse files Browse the repository at this point in the history
Support secrets for paladin
  • Loading branch information
hosie authored Nov 18, 2024
2 parents 77f5db3 + 2f52f78 commit 55c0aff
Show file tree
Hide file tree
Showing 13 changed files with 457 additions and 42 deletions.
1 change: 0 additions & 1 deletion operator/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,6 @@ helm-uninstall-dependencies:
@if $(KUBECTL) version --client=false > /dev/null 2>&1; then \
$(HELM) uninstall cert-manager --namespace cert-manager > /dev/null 2>&1 || true; \
$(KUBECTL) delete namespace cert-manager > /dev/null 2>&1 || true; \
$(KUBECTL) delete -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.1/cert-manager.crds.yaml > /dev/null 2>&1 || true; \
else \
echo "No Kubernetes cluster detected. Skipping dependencies uninstallation."; \
fi
Expand Down
22 changes: 22 additions & 0 deletions operator/api/v1alpha1/paladin_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ type PaladinSpec struct {
// (vs. configuring a connection to a production blockchain network)
BesuNode string `json:"besuNode,omitempty"`

// AuthConfig is used to provide authentication details for blockchain connections
// If this is set, it will override the auth details in the config
AuthConfig *AuthConfig `json:"authConfig,omitempty"`

// Optionally tune the service definition.
// We merge any configuration you add (such as node ports) for the following services:
// "rpc-http" - 8545 (TCP),
Expand Down Expand Up @@ -139,6 +143,24 @@ type SecretBackedSigner struct {
KeySelector string `json:"keySelector"`
}

type AuthMethod string

const AuthMethodSecret AuthMethod = "secret"

type AuthConfig struct {
// auth method to use for the connection
// +kubebuilder:validation:Enum=secret
AuthMethod AuthMethod `json:"authMethod"`

// SecretAuth is used to provide the name of the secret to use for authentication
AuthSecret *AuthSecret `json:"authSecret,omitempty"`
}

type AuthSecret struct {
// The name of the secret to use for authentication
Name string `json:"name"`
}

// StatusReason is an enumeration of possible failure causes. Each StatusReason
// must map to a single HTTP status code, but multiple reasons may map
// to the same HTTP status code.
Expand Down
40 changes: 40 additions & 0 deletions operator/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 46 additions & 38 deletions operator/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,30 @@ def printClusterStatus(String namespace) {
println getAllProcess.in.text.trim()
}

def verifyResourceCreated(String namespace, String resourceType, String resourceName, int timeoutSeconds = 60) {

// Wait for resource creation
def existsCommand = ["kubectl", "wait", "--for=create", "${resourceType}", "${resourceName}", "-n", namespace, "--timeout=${timeoutSeconds}s"]
def existsProcess = existsCommand.execute()
existsProcess.waitFor()
println "Creation Output: ${existsProcess.text}"

if (existsProcess.exitValue() != 0) {
def verifyResourceCreated(String namespace, String resourceType, String resourceName, String desiredReplicas = "1", int timeoutSeconds = 60) {
def startTime = System.currentTimeMillis()
def endTime = startTime + (timeoutSeconds * 1000)
def resourceReady = false

while (System.currentTimeMillis() < endTime) {
def command = ["kubectl", "get", resourceType, resourceName, "-n", namespace, "-o", "jsonpath={.status.readyReplicas}"]
def process = command.execute()
process.waitFor()
def readyReplicas = process.text.trim()
println "Current Ready Replicas: ${readyReplicas}"

if (process.exitValue() == 0 && readyReplicas == desiredReplicas) {
resourceReady = true
break
}
Thread.sleep(2000) // Wait for 2 seconds before retrying
}

if (!resourceReady) {
printClusterStatus(namespace)
throw new Exception("Resource ${resourceType}/${resourceName} (${namespace}) was not created within the expected time.")
throw new Exception("${resourceType} ${resourceName} in namespace ${namespace} did not become ready within ${timeoutSeconds} seconds.")
} else {
println "${resourceType} ${resourceName} in namespace ${namespace} is ready with ${desiredReplicas} replicas."
}
}

Expand Down Expand Up @@ -219,39 +232,34 @@ task installOperator(type: Exec, dependsOn: [installCrds, promoteKindImages, pre
task verifyOperator(dependsOn: installOperator) {
doLast {
println 'Waiting for operator deployment to become ready...'
verifyResourceReady(namespace, 'deployment', 'paladin-operator', 'condition=available')
verifyResourceCreated(namespace, 'deployment', 'paladin-operator', "1")
}
}

// // Task to create the nodes
// task createNode(type: Exec, dependsOn: verifyOperator) {
// executable 'make'
// args 'create-node'
// args "NAMESPACE=${namespace}"
// }

// // Task to verify the besu statefulSet
// task verifyBesu(dependsOn: createNode) {
// doLast {
// println 'Waiting for besu statefulSet to become ready...'

// verifyResourceCreated(namespace, 'statefulset', 'besu-node1', 120)
// verifyResourceReady(namespace, 'statefulset', 'besu-node1', 'jsonpath=.status.readyReplicas=1', 60)
// }
// }

// // Task to verify the paladin statefulSet
// task verifyPaladin(dependsOn: createNode) {
// doLast {
// println 'Waiting for paladin statefulSet to become ready...'

// verifyResourceCreated(namespace, 'statefulset', 'paladin-node1', 120)
// verifyResourceReady(namespace, 'statefulset', 'paladin-node1', 'jsonpath=.status.readyReplicas=1', 60)
// }
// }
// Task to verify the besu statefulSet
task verifyBesu(dependsOn: verifyOperator) {
doLast {
println 'Waiting for besu statefulSet to become ready...'

verifyResourceCreated(namespace, 'statefulset', 'besu-node1', "1", 120)
verifyResourceCreated(namespace, 'statefulset', 'besu-node2', "1", 120)
verifyResourceCreated(namespace, 'statefulset', 'besu-node3', "1", 120)
}
}

// Task to verify the paladin statefulSet
task verifyPaladin(dependsOn: verifyBesu) {
doLast {
println 'Waiting for paladin statefulSet to become ready...'

verifyResourceCreated(namespace, 'statefulset', 'paladin-node1', "1", 120)
verifyResourceCreated(namespace, 'statefulset', 'paladin-node2', "1", 120)
verifyResourceCreated(namespace, 'statefulset', 'paladin-node3', "1", 120)
}
}

// The 'deplay' runs the whole flow
task deploy(dependsOn: [copySolidity, copyZetoSolidity, verifyOperator]) {
task deploy(dependsOn: [copySolidity, copyZetoSolidity, verifyPaladin]) {
doLast {
println 'Deplopy setup completed. Operator is running in the paladin namespace.'
}
Expand Down
4 changes: 2 additions & 2 deletions operator/charts/paladin-operator-crd/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.0
version: 0.0.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.0.0"
appVersion: "0.0.1"
2 changes: 1 addition & 1 deletion operator/charts/paladin-operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ appVersion: "0.0.2"

dependencies:
- name: paladin-operator-crd
version: 0.0.0
version: 0.0.1
repository: "file://../paladin-operator-crd/"
condition: installCRDs

Expand Down
23 changes: 23 additions & 0 deletions operator/config/crd/bases/core.paladin.io_paladins.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,29 @@ spec:
spec:
description: PaladinSpec defines the desired state of Paladin
properties:
authConfig:
description: |-
AuthConfig is used to provide authentication details for blockchain connections
If this is set, it will override the auth details in the config
properties:
authMethod:
description: auth method to use for the connection
enum:
- secret
type: string
authSecret:
description: SecretAuth is used to provide the name of the secret
to use for authentication
properties:
name:
description: The name of the secret to use for authentication
type: string
required:
- name
type: object
required:
- authMethod
type: object
besuNode:
description: |-
Optionally bind to a local besu node deployed with this operator
Expand Down
44 changes: 44 additions & 0 deletions operator/internal/controller/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package controller

import (
"context"
"fmt"
"reflect"
"sort"

corev1alpha1 "github.com/kaleido-io/paladin/operator/api/v1alpha1"
Expand Down Expand Up @@ -165,3 +167,45 @@ func setCondition(
// Update or append the condition
meta.SetStatusCondition(conditions, condition)
}

func mapToStruct(data map[string][]byte, result interface{}) error {
// Ensure that result is a pointer to a struct
v := reflect.ValueOf(result)
if v.Kind() != reflect.Ptr || v.IsNil() {
return fmt.Errorf("result argument must be a non-nil pointer to a struct")
}
v = v.Elem()
if v.Kind() != reflect.Struct {
return fmt.Errorf("result argument must be a pointer to a struct")
}

t := v.Type()

for i := 0; i < v.NumField(); i++ {
fieldValue := v.Field(i)
fieldType := t.Field(i)

// Skip unexported fields
if !fieldValue.CanSet() {
continue
}

// Get the JSON tag or use the field name
tag := fieldType.Tag.Get("json")
if tag == "" {
tag = fieldType.Name
}

// Check if the map contains the key
if val, exists := data[tag]; exists {
switch fieldValue.Kind() {
case reflect.String:
fieldValue.SetString(string(val))
default:
return fmt.Errorf("unsupported field type: %s", fieldValue.Kind())
}
}
}

return nil
}
87 changes: 87 additions & 0 deletions operator/internal/controller/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package controller

import (
"testing"

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

func TestMapToStruct(t *testing.T) {

type example struct {
Username string `json:"username"`
Password string `json:"password"`
}
tests := []struct {
name string
data map[string][]byte
result interface{}
want interface{}
wantErr bool
}{
{
name: "Valid mapping to example",
data: map[string][]byte{
"username": []byte("testuser"),
"password": []byte("testpass"),
},
result: &example{},
want: &example{
Username: "testuser",
Password: "testpass",
},
wantErr: false,
},
{
name: "Missing key in map",
data: map[string][]byte{
"username": []byte("testuser"),
},
result: &example{},
want: &example{
Username: "testuser",
Password: "",
},
wantErr: false,
},
{
name: "Result is not a pointer",
data: map[string][]byte{},
result: example{},
want: nil,
wantErr: true,
},
{
name: "Result is nil pointer",
data: map[string][]byte{},
result: (*example)(nil),
want: nil,
wantErr: true,
},
{
name: "Unsupported field type",
data: map[string][]byte{"unsupported": []byte("value")},
result: &struct {
UnsupportedField int `json:"unsupported"`
}{},
want: nil,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := mapToStruct(tt.data, tt.result)

if tt.wantErr {
assert.Error(t, err, "Expected an error but got none")
} else {
assert.NoError(t, err, "Did not expect an error but got one")
}

if !tt.wantErr {
assert.Equal(t, tt.want, tt.result, "Result mismatch")
}
})
}
}
Loading

0 comments on commit 55c0aff

Please sign in to comment.