Skip to content

Commit

Permalink
Add updateScript field and improve readme.
Browse files Browse the repository at this point in the history
  • Loading branch information
etesami committed Sep 3, 2024
1 parent 60debf8 commit ad0af54
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 66 deletions.
68 changes: 48 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ within the scripts before they are sent to the remote machine.
to the target machine, executing the scripts, and writing the output (`stdout` and `stderr`)
back to the respective status fields of the object.

![image](./provider-ssh-crossplane-diagram.png)
![image](./provider-ssh-crossplane-flowchart.jpg)

## Getting Started

Expand Down Expand Up @@ -89,14 +89,20 @@ spec:

### Script

A `Script` object supports the following:

- `initScript`: This script is executed for the first time and whenever the resource is detected as not existing,
or the `statusCheckScript` returns an error.
- `statusCheckScript`: This script is executed to check the status after running the `initScript`.
A `Script` object supports the following types of scripts:

- `initScript`: This script is executed the first time and whenever the resource is detected as non-existent.
- `statusCheckScript`: This script is executed frequently to check the status of the resource.
The exit status code should correspond to the following conditions:
- `Exit Status Code = 0`: The script executes successfully, and the resource is ready.
- `Exit Status Code = 1`: The script fails, and the resource is not ready. The `statusCheckScript`
will be executed again when the request is requeued.
- `Exit Status Code = 100`: The resource does not exist on the remote machine. The `initScript` will be executed.
- `Exit Status Code = Any Other Value`: The resource is not ready yet, and the `statusCheckScript` will
be executed again.
- `updateScript`: This script is executed based on the exit status code of the `statusCheckScript`.
- `cleanupScript`: This script is executed when the managed resource is deleted.

When any of these scripts are executed, the `statusCode` field reflects the returned status code of the script execution.
The `stdout` and `stderr` fields capture the standard output and standard error,
respectively, of the last execution of the `statusCheckScript`.

Expand All @@ -118,23 +124,45 @@ spec:
echo {{VPN_SERVER_URL}} >> /tmp/new_file.txt
date >> /tmp/new_file.txt
echo "--- --- --- ---" >> /tmp/new_file.txt
cat /tmp/new_file.txt
statusCheckScript: |
# check if the file exists
if [ -f /tmp/new_file.txt ]; then
echo "File exists"
# check if the file has the correct content
if grep -q {{VPN_SERVER_URL}} /tmp/new_file.txt; then
echo "File has the correct content"
exit 0
# Create the script file
cat << 'EOF' > /tmp/prolonged-execution-script.sh
#!/bin/bash
HOST_ACCESSIBLE=false
HOST=google.ca
while [ "$HOST_ACCESSIBLE" = false ]; do
ping -c 1 "$HOST" > /dev/null 2>&1
if [ $? -eq 0 ]; then
HOST_ACCESSIBLE=true
echo "INFO: $HOST is accessible. Attempt $RETRY_COUNT"
else
echo "File does not have the correct content"
exit 1
echo "INFO: Attempt $(($RETRY_COUNT + 1)): $HOST is not accessible. Retrying..."
RETRY_COUNT=$((RETRY_COUNT + 1))
sleep 5
fi
else
done
EOF
chmod +x /tmp/prolonged-execution-script.sh
# Run inside screen to ensure the complete execution of the script
SESSION_NAME="my-script"
screen -dmS $SESSION_NAME
screen -S $SESSION_NAME -X stuff "bash /tmp/prolonged-execution-script.sh^M"
# The exit status code has no effect.
# The statusCheck script will determine if the script has executed successfully.
statusCheckScript: |
# check if the file exists
if [ ! -f /tmp/new_file.txt ]; then
echo "File does not exist"
exit 1
exit 105 # Custom exit code.
# TODO: The exit status code should be made available to updateScript
# so that appropriate actions can be taken.
fi
updateScript: ""
cleanupScript: |
rm /tmp/new_file.txt
sudoEnabled: false
Expand Down
5 changes: 3 additions & 2 deletions apis/v1alpha1/script_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ type Variable struct {
// ScriptParameters are the configurable fields of a Script.
type ScriptParameters struct {
Variables []Variable `json:"variables,omitempty"`
InitScript string `json:"initScript"`
StatusCheckScript string `json:"statusCheckScript,omitempty"`
InitScript string `json:"initScript,omitempty"`
StatusCheckScript string `json:"statusCheckScript"`
UpdateScript string `json:"updateScript,omitempty"`
CleanupScript string `json:"cleanupScript,omitempty"`
SudoEnabled bool `json:"sudoEnabled,omitempty"`
}
Expand Down
2 changes: 0 additions & 2 deletions internal/client/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ func NewSSHClient(ctx context.Context, data []byte) (*ssh.Client, error) { // no

var knownHostsCallback ssh.HostKeyCallback
if kc.KnownHosts != "" {
logger.Info("Using known hosts")
tempFile, err := os.CreateTemp("", "tempfile")
if err != nil {
return nil, errors.Wrap(err, "Failed to create temp file to parse known hosts")
Expand All @@ -89,7 +88,6 @@ func NewSSHClient(ctx context.Context, data []byte) (*ssh.Client, error) { // no

switch {
case kc.PrivateKey != "":
logger.Info("Using private key")
privateKeyBytes, err := base64.StdEncoding.DecodeString(kc.PrivateKey)
if err != nil {
logger.Error(err, "Error decoding base64 private key")
Expand Down
93 changes: 52 additions & 41 deletions internal/controller/script/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,6 @@ const (
errNewClient = "cannot create new Service"
)

// // A NoOpService does nothing.
// type NoOpService struct{}

// var (
// newNoOpService = func(_ []byte) (interface{}, error) { return &NoOpService{}, nil }
// )

// Setup adds a controller that reconciles Script managed resources.
func Setup(mgr ctrl.Manager, o controller.Options) error {
name := managed.ControllerName(apisv1alpha1.ScriptGroupKind)
Expand Down Expand Up @@ -86,8 +79,6 @@ func Setup(mgr ctrl.Manager, o controller.Options) error {
Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter))
}

// A connector is expected to produce an ExternalClient when its Connect method
// is called.
type connector struct {
kube client.Client
usage resource.Tracker
Expand Down Expand Up @@ -164,17 +155,36 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
// init script and the target is not ready yet, or the init script is not
// executed at all. In both cases, we request to run init script again
// by returning ResourceExists: false
logger.Info(fmt.Sprintf("[%s] Observing failed. File does not exist or is not ready yet.", mg.GetName()))
var exitStatus int
if exitErr, ok := err.(*ssh.ExitError); ok {
exitStatus = exitErr.ExitStatus()
} else {
exitStatus = 1
logger.Info(fmt.Sprintf("[%s] Unable to detect exit code", mg.GetName()))
}

logger.Info(fmt.Sprintf("[%s] Observing failed. Exit code: %d", mg.GetName(), exitStatus))
cr.Status.AtProvider.Stdout = stdout
cr.Status.AtProvider.Stderr = stderr
cr.Status.AtProvider.StatusCode = 1

// We don't update. By returning ResourceExists: false, the managed resource
// reconciler will call Create again and the script will be executed again.
// Please note we don't return error here, because the create function will not
// be called if the observe function returns an error.
// TODO: Ensure this logic is the best approach here.
return managed.ExternalObservation{ResourceExists: false, ResourceUpToDate: false}, nil
cr.Status.AtProvider.StatusCode = exitStatus

// if the exit code is 1, it means the script failed. This type of failure
// is not recoverable automatically, so we set the status to ReconcileError.
if exitStatus == 1 {
cr.SetConditions(xpv1.ReconcileError(errors.Wrap(err, "Script failed with exit code 1.")))
return managed.ExternalObservation{}, errors.Wrap(err, "Script failed with exit code 1.")
}

// If the exit code is 100, it means the resources does not exist yet.
if exitStatus == 100 {
return managed.ExternalObservation{ResourceExists: false}, nil
}

// If the exit code is a custom applecation-specific code, it means the script
// failed but the failure may be recoverable. The recovery should be handled by
// the update script. We don't return error here, as the update does not get called
// instead we update resource status fields with returned stdout, stderr and exit code.
return managed.ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil
}

logger.Info(fmt.Sprintf("[%s] Observing was [okay]. Update the status.", mg.GetName()))
Expand All @@ -187,20 +197,9 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex

logger.Info(fmt.Sprintf("[%s] Observing, no status check script.", mg.GetName()))

// If the StatusCheckScript is not set, there is nothing to run.
return managed.ExternalObservation{
// Return false when the external resource does not exist. This lets
// the managed resource reconciler know that it needs to call Create to
// (re)create the resource, or that it has successfully been deleted.
ResourceExists: true,

// Return false when the external resource exists, but it not up to date
// with the desired managed resource state. This lets the managed
// resource reconciler know that it needs to call Update.
ResourceUpToDate: true,

// Return any details that may be required to connect to the external
// resource. These will be stored as the connection secret.
ResourceExists: true,
ResourceUpToDate: true,
ConnectionDetails: managed.ConnectionDetails{},
}, nil
}
Expand All @@ -214,31 +213,43 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext
}

if cr.Spec.ForProvider.InitScript != "" {
// TODO: There may be output for init script, how do we handle it?
_, _, err := sshv1alpha1.ExecuteScript(
ctx, c.service.(*ssh.Client), cr.Spec.ForProvider.InitScript, cr.Spec.ForProvider.Variables, cr.Spec.ForProvider.SudoEnabled)
if err != nil {
// If the script fails, it means there is either an issue with the
// init script and the target is not ready yet, or the init script is not
// executed at all.
cr.SetConditions(xpv1.Unavailable())
// executed at all. By returning error here, the reconciler will not proceed,
// and user intervention is required.
cr.SetConditions(xpv1.ReconcileError(errors.Wrap(err, "Init Script failed.")))
return managed.ExternalCreation{}, err
} else {
cr.Status.AtProvider.StatusCode = 0
}
}
return managed.ExternalCreation{
// Optionally return any details that may be required to connect to the
// external resource. These will be stored as the connection secret.
ConnectionDetails: managed.ConnectionDetails{},
}, nil
}

func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) {
// The function update does not do anything.
logger := log.FromContext(ctx).WithName("[UPDATE]")
logger.Info(fmt.Sprintf("[%s] Updating resource...", mg.GetName()))
cr, ok := mg.(*apisv1alpha1.Script)
if !ok {
return managed.ExternalUpdate{}, errors.New(errNotScript)
}

if cr.Spec.ForProvider.UpdateScript != "" {
_, _, err := sshv1alpha1.ExecuteScript(
ctx, c.service.(*ssh.Client), cr.Spec.ForProvider.UpdateScript, cr.Spec.ForProvider.Variables, cr.Spec.ForProvider.SudoEnabled)
if err != nil {
// the update script is supposed to return error if the update fails and is not recoverable.
// If we return error here, the reconcile will not proceed, and user intervention is required.
cr.SetConditions(xpv1.ReconcileError(errors.Wrap(err, "Update Script failed.")))
return managed.ExternalUpdate{}, err
}
}
// If there is no update script, or the update does not encounter any error, we return success.
// and we will observe the resource again to check if the update was successful.
return managed.ExternalUpdate{
// Optionally return any details that may be required to connect to the
// external resource. These will be stored as the connection secret.
ConnectionDetails: managed.ConnectionDetails{},
}, nil
}
Expand Down
4 changes: 3 additions & 1 deletion package/crds/ssh.crossplane.io_scripts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ spec:
type: string
sudoEnabled:
type: boolean
updateScript:
type: string
variables:
items:
properties:
Expand All @@ -96,7 +98,7 @@ spec:
type: object
type: array
required:
- initScript
- statusCheckScript
type: object
managementPolicies:
default:
Expand Down
Binary file removed provider-ssh-crossplane-diagram.png
Binary file not shown.
Binary file added provider-ssh-crossplane-flowchart.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit ad0af54

Please sign in to comment.