renoki-co/php-k8s

View on GitHub
src/Traits/RunsClusterOperations.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php

namespace RenokiCo\PhpK8s\Traits;

use Closure;
use RenokiCo\PhpK8s\Contracts\Attachable;
use RenokiCo\PhpK8s\Contracts\Executable;
use RenokiCo\PhpK8s\Contracts\Loggable;
use RenokiCo\PhpK8s\Contracts\Scalable;
use RenokiCo\PhpK8s\Contracts\Watchable;
use RenokiCo\PhpK8s\Exceptions\KubernetesAPIException;
use RenokiCo\PhpK8s\Exceptions\KubernetesAttachException;
use RenokiCo\PhpK8s\Exceptions\KubernetesExecException;
use RenokiCo\PhpK8s\Exceptions\KubernetesLogsException;
use RenokiCo\PhpK8s\Exceptions\KubernetesScalingException;
use RenokiCo\PhpK8s\Exceptions\KubernetesWatchException;
use RenokiCo\PhpK8s\Kinds\K8sScale;
use RenokiCo\PhpK8s\KubernetesCluster;

trait RunsClusterOperations
{
    use Resource\HasAttributes;
    use Resource\HasNamespace;

    /**
     * The cluster instance that
     * binds to the cluster API.
     *
     * @var \RenokiCo\PhpK8s\KubernetesCluster
     */
    protected $cluster;

    /**
     * Specify the cluster to attach to.
     *
     * @param  \RenokiCo\PhpK8s\KubernetesCluster  $cluster
     * @return $this
     */
    public function onCluster(KubernetesCluster $cluster)
    {
        $this->cluster = $cluster;

        return $this;
    }

    /**
     * Get the resource version of the resource.
     *
     * @return string|null
     */
    public function getResourceVersion()
    {
        return $this->getAttribute('metadata.resourceVersion', null);
    }

    /**
     * Get the resource UID.
     *
     * @return string|null
     */
    public function getResourceUid()
    {
        return $this->getAttribute('metadata.uid', null);
    }

    /**
     * Get the identifier for the current resource.
     *
     * @return mixed
     */
    public function getIdentifier()
    {
        return $this->getAttribute('metadata.name', null);
    }

    /**
     * Make a call to the cluster to get a fresh instance.
     *
     * @param  array  $query
     * @return $this
     */
    public function refresh(array $query = ['pretty' => 1])
    {
        return $this->syncWith($this->get($query)->toArray());
    }

    /**
     * Make a call to the cluster to get fresh original values.
     *
     * @param  array  $query
     * @return $this
     */
    public function refreshOriginal(array $query = ['pretty' => 1])
    {
        return $this->syncOriginalWith($this->get($query)->toArray());
    }

    /**
     * Make sure to sync the resource version with the original.
     *
     * @return $this
     */
    public function refreshResourceVersion()
    {
        $this->setAttribute(
            'metadata.resourceVersion',
            $this->original['metadata']['resourceVersion']
        );

        return $this;
    }

    /**
     * Create or update the resource, wether the resource exists
     * or not within the cluster.
     *
     * @param  array  $query
     * @return $this
     */
    public function syncWithCluster(array $query = ['pretty' => 1])
    {
        try {
            return $this->get($query);
        } catch (KubernetesAPIException $e) {
            return $this->create($query);
        }
    }

    /**
     * Create or update the app based on existence.
     *
     * @param  array  $query
     * @return $this
     */
    public function createOrUpdate(array $query = ['pretty' => 1])
    {
        if ($this->exists($query)) {
            $this->update($query);

            return $this;
        }

        return $this->create($query);
    }

    /**
     * Get a list with all resources.
     *
     * @param  array  $query
     * @return \RenokiCo\PhpK8s\ResourcesList
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAPIException
     */
    public function all(array $query = ['pretty' => 1])
    {
        return $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::GET_OP,
                $this->allResourcesPath(),
                $this->toJsonPayload(),
                $query
            );
    }

    /**
     * Get a list with all resources from all namespaces.
     *
     * @param  array  $query
     * @return \RenokiCo\PhpK8s\ResourcesList
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAPIException
     */
    public function allNamespaces(array $query = ['pretty' => 1])
    {
        return $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::GET_OP,
                $this->allResourcesPath(false),
                $this->toJsonPayload(),
                $query
            );
    }

    /**
     * Get a fresh instance from the cluster.
     *
     * @param  array  $query
     * @return \RenokiCo\PhpK8s\Kinds\K8sResource
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAPIException
     */
    public function get(array $query = ['pretty' => 1])
    {
        return $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::GET_OP,
                $this->resourcePath(),
                $this->toJsonPayload(),
                $query
            );
    }

    /**
     * Create the resource.
     *
     * @param  array  $query
     * @return \RenokiCo\PhpK8s\Kinds\K8sResource
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAPIException
     */
    public function create(array $query = ['pretty' => 1])
    {
        return $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::CREATE_OP,
                $this->allResourcesPath(),
                $this->toJsonPayload(),
                $query
            );
    }

    /**
     * Update the resource.
     *
     * @param  array  $query
     * @return bool
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAPIException
     */
    public function update(array $query = ['pretty' => 1]): bool
    {
        $this->refreshOriginal();
        $this->refreshResourceVersion();

        // If it didn't change, no way to trigger the change.
        if (! $this->hasChanged()) {
            return true;
        }

        $instance = $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::REPLACE_OP,
                $this->resourcePath(),
                $this->toJsonPayload(),
                $query
            );

        $this->syncWith($instance->toArray());

        return true;
    }

    /**
     * Delete the resource.
     *
     * @param  array  $query
     * @param  null|int  $gracePeriod
     * @param  string  $propagationPolicy
     * @return bool
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAPIException
     */
    public function delete(array $query = ['pretty' => 1], $gracePeriod = null, string $propagationPolicy = 'Foreground'): bool
    {
        if (! $this->isSynced()) {
            return true;
        }

        $this->setAttribute('preconditions', [
            'resourceVersion' => $this->getResourceVersion(),
            'uid' => $this->getResourceUid(),
            'propagationPolicy' => $propagationPolicy,
            'gracePeriodSeconds' => $gracePeriod,
        ]);

        $this->refresh();

        $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::DELETE_OP,
                $this->resourcePath(),
                $this->toJsonPayload('DeleteOptions'),
                $query
            );

        $this->synced = false;

        return true;
    }

    /**
     * Watch the resources list until the closure returns true or false.
     *
     * @param  Closure  $callback
     * @param  array  $query
     * @return mixed
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesWatchException
     */
    public function watchAll(Closure $callback, array $query = ['pretty' => 1])
    {
        if (! $this instanceof Watchable) {
            throw new KubernetesWatchException(
                'The resource '.get_class($this).' does not support watch actions.'
            );
        }

        return $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::WATCH_OP,
                $this->allResourcesWatchPath(),
                $callback,
                $query
            );
    }

    /**
     * Watch the specific resource until the closure returns true or false.
     *
     * @param  Closure  $callback
     * @param  array  $query
     * @return mixed
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesWatchException
     */
    public function watch(Closure $callback, array $query = ['pretty' => 1])
    {
        if (! $this instanceof Watchable) {
            throw new KubernetesWatchException(
                'The resource '.get_class($this).' does not support watch actions.'
            );
        }

        return $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::WATCH_OP,
                $this->resourceWatchPath(),
                $callback,
                $query
            );
    }

    /**
     * Get a specific resource's logs.
     *
     * @param  array  $query
     * @return string
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesLogsException
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAPIException
     */
    public function logs(array $query = ['pretty' => 1])
    {
        if (! $this instanceof Loggable) {
            throw new KubernetesLogsException(
                'The resource '.get_class($this).' does not support logs.'
            );
        }

        return $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::LOG_OP,
                $this->resourceLogPath(),
                $this->toJsonPayload(),
                $query
            );
    }

    /**
     * Watch the specific resource's logs until the closure returns true or false.
     *
     * @param  Closure  $callback
     * @param  array  $query
     * @return mixed
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesWatchException
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesLogsException
     */
    public function watchLogs(Closure $callback, array $query = ['pretty' => 1])
    {
        if (! $this instanceof Loggable) {
            throw new KubernetesWatchException(
                'The resource '.get_class($this).' does not support logs.'
            );
        }

        if (! $this instanceof Watchable) {
            throw new KubernetesLogsException(
                'The resource '.get_class($this).' does not support watch actions.'
            );
        }

        // Ensure the ?follow=1 query exists to trigger the watch.
        $query = array_merge($query, ['follow' => 1]);

        return $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::WATCH_LOGS_OP,
                $this->resourceLogPath(),
                $callback,
                $query
            );
    }

    /**
     * Get a specific resource scaling data.
     *
     * @return \RenokiCo\PhpK8s\Kinds\K8sScale
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesScalingException
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAPIException
     */
    public function scaler(): K8sScale
    {
        if (! $this instanceof Scalable) {
            throw new KubernetesScalingException(
                'The resource '.get_class($this).' does not support scaling.'
            );
        }

        $scaler = $this->cluster
            ->setResourceClass(K8sScale::class)
            ->runOperation(
                KubernetesCluster::GET_OP,
                $this->resourceScalePath(),
                $this->toJsonPayload(),
                ['pretty' => 1]
            );

        $scaler->setScalableResource($this);

        return $scaler;
    }

    /**
     * Exec a command on the current resource.
     *
     * @param  string|array  $command
     * @param  string|null  $container
     * @param  array  $query
     * @return string
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesExecException
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAPIException
     */
    public function exec(
        $command,
        string $container = null,
        array $query = ['pretty' => 1, 'stdin' => 1, 'stdout' => 1, 'stderr' => 1, 'tty' => 1]
    ) {
        if (! $this instanceof Executable) {
            throw new KubernetesExecException(
                'The resource '.get_class($this).' does not support exec commands.'
            );
        }

        return $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::EXEC_OP,
                $this->resourceExecPath(),
                '',
                ['command' => array_map('urlencode', $command), 'container' => $container] + $query
            );
    }

    /**
     * Attach to the current resource.
     *
     * @param  \Closure|null  $callback
     * @param  string|null  $container
     * @param  array  $query
     * @return string
     *
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAttachException
     * @throws \RenokiCo\PhpK8s\Exceptions\KubernetesAPIException
     */
    public function attach(
        Closure $callback = null,
        string $container = null,
        array $query = ['pretty' => 1, 'stdin' => 1, 'stdout' => 1, 'stderr' => 1, 'tty' => 1]
    ) {
        if (! $this instanceof Attachable) {
            throw new KubernetesAttachException(
                'The resource '.get_class($this).' does not support attach commands.'
            );
        }

        return $this->cluster
            ->setResourceClass(get_class($this))
            ->runOperation(
                KubernetesCluster::ATTACH_OP,
                $this->resourceAttachPath(),
                $callback,
                ['container' => $container] + $query
            );
    }

    /**
     * Get the path, prefixed by '/', that points to the resources list.
     *
     * @param  bool  $withNamespace
     * @return string
     */
    public function allResourcesPath(bool $withNamespace = true): string
    {
        return "{$this->getApiPathPrefix($withNamespace)}/".static::getPlural();
    }

    /**
     * Get the path, prefixed by '/', that points to the specific resource.
     *
     * @return string
     */
    public function resourcePath(): string
    {
        return "{$this->getApiPathPrefix()}/".static::getPlural()."/{$this->getIdentifier()}";
    }

    /**
     * Get the path, prefixed by '/', that points to the resource watch.
     *
     * @return string
     */
    public function allResourcesWatchPath(): string
    {
        return "{$this->getApiPathPrefix(false)}/watch/".static::getPlural();
    }

    /**
     * Get the path, prefixed by '/', that points to the specific resource to watch.
     *
     * @return string
     */
    public function resourceWatchPath(): string
    {
        return "{$this->getApiPathPrefix(true, 'watch')}/".static::getPlural()."/{$this->getIdentifier()}";
    }

    /**
     * Get the path, prefixed by '/', that points to the resource scale.
     *
     * @return string
     */
    public function resourceScalePath(): string
    {
        return "{$this->getApiPathPrefix()}/".static::getPlural()."/{$this->getIdentifier()}/scale";
    }

    /**
     * Get the path, prefixed by '/', that points to the specific resource to log.
     *
     * @return string
     */
    public function resourceLogPath(): string
    {
        return "{$this->getApiPathPrefix()}/".static::getPlural()."/{$this->getIdentifier()}/log";
    }

    /**
     * Get the path, prefixed by '/', that points to the specific resource to exec.
     *
     * @return string
     */
    public function resourceExecPath(): string
    {
        return "{$this->getApiPathPrefix()}/".static::getPlural()."/{$this->getIdentifier()}/exec";
    }

    /**
     * Get the path, prefixed by '/', that points to the specific resource to attach.
     *
     * @return string
     */
    public function resourceAttachPath(): string
    {
        return "{$this->getApiPathPrefix()}/".static::getPlural()."/{$this->getIdentifier()}/attach";
    }

    /**
     * Get the prefix path for the resource.
     *
     * @param  bool  $withNamespace
     * @param  string|null  $preNamespaceAction
     * @return string
     */
    protected function getApiPathPrefix(bool $withNamespace = true, string $preNamespaceAction = null): string
    {
        $version = $this->getApiVersion();

        $path = $version === 'v1' ? '/api/v1' : "/apis/{$version}";

        if ($preNamespaceAction) {
            $path .= "/{$preNamespaceAction}";
        }

        if ($withNamespace && static::$namespaceable) {
            $path .= "/namespaces/{$this->getNamespace()}";
        }

        return $path;
    }
}