diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..e626e6f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# CODEOWNERS file allows us to set owners for specific files and directories +# https://docs.github.com/en/enterprise-cloud@latest/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..11416cc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +version: 2 +updates: + # Maintain dependencies of docker images + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + + # Maintain dependencies of GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + commit-message: + prefix: "ci:" + schedule: + interval: "daily" + labels: + - "Type: Dependencies" + - "Component: CI/CD" + + # Maintain dependencies of elixir applications + - package-ecosystem: "mix" + directory: "/" + commit-message: + prefix: "build:" + schedule: + interval: "daily" + labels: + - "Type: Dependencies" + - "Implementation: Elixir" + groups: + elixir-deps: + patterns: + - "*" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b39b965 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,36 @@ +## Motivation + + + +... + +## Proposed changes + + + +... + +## Unrelated changes (optional) + + + +_None_ + +## Testing / Validation + + + +... + +## Notes (optional) + + + +_None_ + +## Dependencies (optional) + + + +_None_ \ No newline at end of file diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml deleted file mode 100644 index 84a716c..0000000 --- a/.github/workflows/docker-build-push.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Docker Build and Push - -on: - push: - tags: [ "v*" ] - -jobs: - build-and-push: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Extract version from tag - if: startsWith(github.ref, 'refs/tags/') - id: extract_version - run: | - VERSION=${GITHUB_REF#refs/tags/} - VERSION=${VERSION#v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Build and push - uses: docker/build-push-action@v4 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: | - colharris/drowzee:latest - colharris/drowzee:${{ github.sha }} - ${{ steps.extract_version.outputs.version && format('colharris/drowzee:{0}', steps.extract_version.outputs.version) || '' }} diff --git a/.github/workflows/helm-publish.yml b/.github/workflows/helm-publish.yml deleted file mode 100644 index f83370a..0000000 --- a/.github/workflows/helm-publish.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Helm Chart Publisher - -on: - push: - tags: [ "v*" ] - -jobs: - publish: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Extract version from tag - if: startsWith(github.ref, 'refs/tags/') - id: extract_version - run: | - VERSION=${GITHUB_REF#refs/tags/} - VERSION=${VERSION#v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Update Chart Versions - run: | - sed -i "s/^version: .*/version: ${{ steps.extract_version.outputs.version }}/" chart/Chart.yaml - sed -i "s/^appVersion: .*/appVersion: \"${{ steps.extract_version.outputs.version }}\"/" chart/Chart.yaml - - - name: Retrieve existing charts - run: | - git clone --branch gh-pages https://github.com/col/drowzee.git existing-charts - mkdir -p packaged-charts - cp -n existing-charts/*.tgz packaged-charts/ || true - - - name: Set up Helm - uses: azure/setup-helm@v3 - - - name: Package Chart - run: helm package ./chart --destination ./packaged-charts - - - name: Generate Repository Index - run: | - helm repo index ./packaged-charts --url https://col.github.io/drowzee - - - name: Configure Git - run: | - git config user.name "GitHub Actions" - git config user.email "actions@github.com" - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./packaged-charts - keep_files: true - force_orphan: true - destination_dir: . diff --git a/.github/workflows/pipeline-build.yml b/.github/workflows/pipeline-build.yml new file mode 100644 index 0000000..4646bbf --- /dev/null +++ b/.github/workflows/pipeline-build.yml @@ -0,0 +1,136 @@ +name: Build + +on: + workflow_dispatch: + push: + branches: + - feature/sts_jobs + +concurrency: + # One build per PR, do not cancel builds on main + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + HOME: ${{github.workspace}} + +jobs: + generate-version: + runs-on: ubuntu-latest + outputs: + service-version: ${{ steps.version.outputs.new_version }} + chart-version: ${{ steps.chart_version.outputs.new_version }} + steps: + - uses: actions/checkout@v4 + - id: version + uses: chirpwireless/reusable-github-actions/.github/actions/calendar-version@main + - name: Chart Version + id: chart_version + run: | + BASE_VERSION=$(grep '^version:' chart/Chart.yaml | awk '{print $2}') + if [[ "${GITHUB_REF##*/}" == "main" ]]; then + VERSION="$BASE_VERSION" + else + VERSION="${BASE_VERSION}-${{steps.version.outputs.new_version}}" + fi + echo "new_version=$VERSION" >> $GITHUB_OUTPUT + - name: "Create a summary" + run: | + echo "Building a new revision ${{steps.version.outputs.new_version}}" >> $GITHUB_STEP_SUMMARY + echo "Building a new chart revision ${{steps.chart_version.outputs.new_version}}" >> $GITHUB_STEP_SUMMARY + + build-docker-image: + runs-on: ubuntu-latest + needs: [generate-version] + permissions: + packages: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ghcr.io/${{ github.repository_owner }}/drowzee:${{ needs.generate-version.outputs.service-version }} + + build-helm-chart: + runs-on: ubuntu-latest + needs: [generate-version, build-docker-image] + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Update Chart Versions + run: | + sed -i "s/^version: .*/version: ${{ needs.generate-version.outputs.chart-version }}/" chart/Chart.yaml + sed -i "s/^appVersion: .*/appVersion: \"${{ needs.generate-version.outputs.service-version }}\"/" chart/Chart.yaml + - name: Retrieve existing charts + run: | + git clone --branch gh-pages https://github.com/${{ github.repository_owner }}/drowzee.git existing-charts || true + mkdir -p packaged-charts + + if [[ "${GITHUB_REF##*/}" == "main" ]]; then + for file in existing-charts/*; do + if [[ ! $(basename "$file") == drowzee-${{ needs.generate-version.outputs.chart-version }}-* ]]; then + cp "$file" packaged-charts/ + fi + done + else + cp -n existing-charts/*.tgz packaged-charts/ || true + fi + - name: Set up Helm + uses: azure/setup-helm@v4 + - name: Package Chart + run: helm package ./chart --destination ./packaged-charts + - name: Generate Repository Index + run: | + helm repo index ./packaged-charts --url https://${{ github.repository_owner }}.github.io/drowzee + # Popular action to deploy to GitHub Pages: + # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + # Build output to publish to the `gh-pages` branch: + publish_dir: ./packaged-charts + keep_files: true + force_orphan: true + destination_dir: . + # The following lines assign commit authorship to the official + # GH-Actions bot for deploys to `gh-pages` branch: + # https://github.com/actions/checkout/issues/13#issuecomment-724415212 + # The GH actions bot is used by default if you didn't specify the two fields. + # You can swap them out with your own user credentials. + user_name: github-actions[bot] + user_email: 41898282+github-actions[bot]@users.noreply.github.com + + publish-version: + runs-on: ubuntu-latest + needs: [generate-version, build-docker-image, build-helm-chart] + permissions: + checks: write + steps: + - name: Publish Version + uses: chirpwireless/reusable-github-actions/.github/actions/version-publish@main + with: + commit-sha: ${{ github.event.pull_request.head.sha || github.sha }} + build-version: ${{ needs.generate-version.outputs.service-version }} + + publish-release: + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + needs: [generate-version, build-docker-image, build-helm-chart] + steps: + - uses: chirpwireless/reusable-github-actions/.github/actions/publish-release@main + with: + version: ${{ needs.generate-version.outputs.service-version }} diff --git a/.gitignore b/.gitignore index 87d00ac..cc98d4d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,24 @@ drowzee-*.tar npm-debug.log /assets/node_modules/ +# Elixir Language Server +.elixir_ls/ + +# Environment files +.env +.env.* + +# Editor swap/backup files +*.swp +*.swo +*.bak + +# Generic log files +*.log + +# macOS .DS_Store + +# Local config/log files local_values.yaml -dorwzee-logs.txt +drowzee-logs.txt diff --git a/Dockerfile b/Dockerfile index 864db07..32d7471 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ENV MIX_ENV=prod \ # && apt-get clean && rm -f /var/lib/apt/lists/*_* RUN mix local.hex --force && \ - mix local.rebar --force + mix local.rebar --force WORKDIR /app diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 3360b7c..a0b64db 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -15,10 +15,10 @@ 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.1.8 +version: 0.2.0 # 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.1.8" +appVersion: "0.1.0" diff --git a/chart/crds/sleepschedule.yaml b/chart/crds/sleepschedule.yaml index ab4b2e7..ea6d73b 100644 --- a/chart/crds/sleepschedule.yaml +++ b/chart/crds/sleepschedule.yaml @@ -33,8 +33,8 @@ spec: status: type: string enum: - - 'True' - - 'False' + - "True" + - "False" type: type: string reason: @@ -52,9 +52,11 @@ spec: required: - sleepTime - timezone - - wakeTime - - deployments properties: + enabled: + type: boolean + description: Whether the schedule is enabled + default: true deployments: type: array description: The deployments that will be slept/woken. @@ -65,20 +67,44 @@ spec: properties: name: type: string + statefulsets: + type: array + description: The statefulsets that will be slept/woken. + items: + type: object + required: + - name + properties: + name: + type: string + cronjobs: + type: array + description: The cronjobs that will be suspended/resumed. + items: + type: object + required: + - name + properties: + name: + type: string ingressName: type: string description: The ingress that will be slept/woken. sleepTime: type: string - description: 'The time that the deployment will start sleeping(format: HH:MMam/pm)' + description: "The time that the deployment will start sleeping(format: HH:MMam/pm)" timezone: type: string description: The timezone that the input times are based in wakeTime: type: string - description: 'The time that the deployment will wake up (format: HH:MMam/pm)' + description: "The time that the deployment will wake up (format: HH:MMam/pm)" storage: true additionalPrinterColumns: + - name: Enabled + type: boolean + description: Enabled + jsonPath: .spec.enabled - name: SleepTime type: string description: Starts Sleeping @@ -95,6 +121,14 @@ spec: type: string description: Deployments jsonPath: .spec.deployments[*].name + - name: StatefulSets + type: string + description: StatefulSets + jsonPath: .spec.statefulsets[*].name + - name: CronJobs + type: string + description: CronJobs + jsonPath: .spec.cronjobs[*].name - name: Sleeping? type: string description: Current Status @@ -114,4 +148,4 @@ metadata: labels: k8s-app: drowzee kind: CustomResourceDefinition -apiVersion: apiextensions.k8s.io/v1 \ No newline at end of file +apiVersion: apiextensions.k8s.io/v1 diff --git a/chart/templates/clusterrole.yaml b/chart/templates/clusterrole.yaml index 319d3ba..03a9085 100644 --- a/chart/templates/clusterrole.yaml +++ b/chart/templates/clusterrole.yaml @@ -38,8 +38,11 @@ rules: - '*' - resources: - deployments + - statefulsets + - cronjobs apiGroups: - "apps" + - "batch" verbs: - '*' - resources: diff --git a/chart/templates/role.yaml b/chart/templates/role.yaml index dca3ccd..9de125e 100644 --- a/chart/templates/role.yaml +++ b/chart/templates/role.yaml @@ -38,8 +38,11 @@ rules: - '*' - resources: - deployments + - statefulsets + - cronjobs apiGroups: - "apps" + - "batch" verbs: - '*' - resources: diff --git a/chart/values.yaml b/chart/values.yaml index 55ed0e8..6fb175d 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -5,7 +5,7 @@ # Can either be `single_namespace` to manage the Drowzee release namespace or `cluster` to manage multiple namespaces mode: "single_namespace" -app: +app: # The host name for the application host: "drowzee.local" server: true @@ -18,7 +18,7 @@ app: config: [] secrets: - # Required - A random string used for signing and encrypting session data and cookies. + # Required - A random string used for signing and encrypting session data and cookies. # Can be generated with `openssl rand -hex 64` secretKeyBase: "" @@ -58,10 +58,12 @@ podAnnotations: {} # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ podLabels: {} -podSecurityContext: {} +podSecurityContext: + {} # fsGroup: 2000 -securityContext: {} +securityContext: + {} # capabilities: # drop: # - ALL @@ -80,7 +82,8 @@ service: ingress: enabled: false className: "" - annotations: {} + annotations: + {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: @@ -93,7 +96,8 @@ ingress: # hosts: # - chart-example.local -resources: {} +resources: + {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following diff --git a/config/config.exs b/config/config.exs index 5c30784..621b1c8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -48,7 +48,7 @@ config :tailwind, # Configures Elixir's Logger config :logger, :console, format: "$time [$level] $message $metadata\n", - metadata: [:name, :namespace, :deployment, :replicas, :ingress_name, :configmap_name, :request_id] + metadata: [:name, :namespace, :replicas, :schedule, :request_id] # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason diff --git a/lib/drowzee/config.ex b/lib/drowzee/config.ex index 5dc31cc..e2cea27 100644 --- a/lib/drowzee/config.ex +++ b/lib/drowzee/config.ex @@ -4,12 +4,29 @@ defmodule Drowzee.Config do """ def namespaces() do - Bonny.Config.namespace() - |> String.split(",") - |> Enum.map(&String.trim/1) + namespace = Bonny.Config.namespace() + + if is_atom(namespace) do + # Handle the atom case - keep :all as is + [namespace] + else + # Handle the string case + if namespace == "__ALL__" do + [:all] + else + namespace + |> String.split(",") + |> Enum.map(&String.trim/1) + end + end end def has_cluster_role?() do - Bonny.Config.namespace() == "__ALL__" or namespaces() |> Enum.count() > 1 + namespace = Bonny.Config.namespace() + # Convert namespace to atom if it's a binary string to allow comparison with :all + # This ensures binary() == :all doesn't happen directly + is_atom(namespace) and namespace == :all or + namespace == "__ALL__" or + namespaces() |> Enum.count() > 1 end end diff --git a/lib/drowzee/controllers/sleep_schedule_controller.ex b/lib/drowzee/controllers/sleep_schedule_controller.ex index 3d26be5..c294a3b 100644 --- a/lib/drowzee/controllers/sleep_schedule_controller.ex +++ b/lib/drowzee/controllers/sleep_schedule_controller.ex @@ -6,15 +6,29 @@ defmodule Drowzee.Controller.SleepScheduleController do use Bonny.ControllerV2 import Drowzee.Axn require Logger - alias Drowzee.K8s.{SleepSchedule, Ingress, Deployment} + alias Drowzee.K8s.{SleepSchedule, Ingress, Deployment, StatefulSet, CronJob} step Bonny.Pluggable.SkipObservedGenerations step :handle_event - def handle_event(%Bonny.Axn{action: action} = axn, _opts) + def handle_event(%Bonny.Axn{resource: %{"spec" => spec}} = axn, _opts) do + Logger.metadata( + schedule: axn.resource["metadata"]["name"], + namespace: axn.resource["metadata"]["namespace"] + ) + + if Map.get(spec, "enabled", true) == false do + Logger.info("Schedule is disabled. Skipping...") + axn + else + handle_event_enabled(axn) + end + end + + def handle_event_enabled(%Bonny.Axn{action: action} = axn) when action in [:add, :modify, :reconcile] do Logger.metadata( - name: axn.resource["metadata"]["name"], + schedule: axn.resource["metadata"]["name"], namespace: axn.resource["metadata"]["namespace"] ) axn @@ -26,10 +40,10 @@ defmodule Drowzee.Controller.SleepScheduleController do end # delete the resource - def handle_event(%Bonny.Axn{action: :delete} = axn, _opts) do + def handle_event_enabled(%Bonny.Axn{action: :delete} = axn, _opts) do Logger.warning("Delete Action - Not yet implemented!") # TODO: - # - Make sure deployments are awake + # - Make sure applications are awake axn end @@ -54,6 +68,12 @@ defmodule Drowzee.Controller.SleepScheduleController do sleep_time = resource["spec"]["sleepTime"] wake_time = resource["spec"]["wakeTime"] timezone = resource["spec"]["timezone"] + + # Log if wake_time is not defined (resources will remain down indefinitely) + if is_nil(wake_time) or wake_time == "" do + Logger.info("Wake time is not defined. Resources will remain scaled down/suspended indefinitely.") + end + case Drowzee.SleepChecker.naptime?(sleep_time, wake_time, timezone) do {:ok, naptime} -> %{axn | assigns: Map.put(axn.assigns, :naptime, naptime)} @@ -110,7 +130,7 @@ defmodule Drowzee.Controller.SleepScheduleController do axn |> update_hosts() |> set_condition("Transitioning", true, "Sleeping", "Going to sleep") - |> scale_down_deployments() + |> scale_down_applications() |> start_transition_monitor() end @@ -118,7 +138,7 @@ defmodule Drowzee.Controller.SleepScheduleController do Logger.info("Initiating wake up") axn |> set_condition("Transitioning", true, "WakingUp", "Waking up") - |> scale_up_deployments() + |> scale_up_applications() |> start_transition_monitor() end @@ -127,13 +147,17 @@ defmodule Drowzee.Controller.SleepScheduleController do axn end - defp scale_down_deployments(axn) do + defp scale_down_applications(axn) do SleepSchedule.scale_down_deployments(axn.resource) + SleepSchedule.scale_down_statefulsets(axn.resource) + SleepSchedule.suspend_cronjobs(axn.resource) axn end - defp scale_up_deployments(axn) do + defp scale_up_applications(axn) do SleepSchedule.scale_up_deployments(axn.resource) + SleepSchedule.scale_up_statefulsets(axn.resource) + SleepSchedule.resume_cronjobs(axn.resource) axn end @@ -155,72 +179,147 @@ defmodule Drowzee.Controller.SleepScheduleController do defp check_sleep_transition(axn, opts) do Logger.info("Checking sleep transition...") - case check_deployment_status(axn, &deployment_status_asleep?/1) do + case check_application_status(axn, &application_status_asleep?/1) do {:ok, true} -> - Logger.debug("All deployments are asleep") + Logger.debug("All applications are asleep") axn |> put_ingress_to_sleep() |> complete_sleep_transition(opts) {:ok, false} -> - Logger.debug("Deployments have not yet scaled down...") - scale_down_deployments(axn) + Logger.debug("Applications have not yet scaled down...") + scale_down_applications(axn) {:error, [error | _]} -> - Logger.error("Failed to check deployment status: #{inspect(error)}") - set_condition(axn, "Error", true, "DeploymentNotFound", error.message) + Logger.error("Failed to check application status: #{inspect(error)}") + set_condition(axn, "Error", true, "ApplicationNotFound", error.message) {:error, error} -> - Logger.error("Failed to check deployment status: #{inspect(error)}") + Logger.error("Failed to check application status: #{inspect(error)}") axn end end defp check_wake_up_transition(axn, opts) do Logger.info("Checking wake up transition...") - case check_deployment_status(axn, &deployment_status_ready?/1) do + case check_application_status(axn, &application_status_ready?/1) do {:ok, true} -> - Logger.debug("All deployments are ready") + Logger.debug("All applications are ready") axn |> wake_up_ingress() |> complete_wake_up_transition(opts) {:ok, false} -> - Logger.debug("Deployments are not ready...") - scale_up_deployments(axn) + Logger.debug("Applications are not ready...") + scale_up_applications(axn) {:error, [error | _]} -> - Logger.error("Failed to check deployment status: #{inspect(error)}") - set_condition(axn, "Error", true, "DeploymentNotFound", error.message) + Logger.error("Failed to check application status: #{inspect(error)}") + set_condition(axn, "Error", true, "ApplicationNotFound", error.message) {:error, error} -> - Logger.error("Failed to check deployment status: #{inspect(error)}") + Logger.error("Failed to check applications status: #{inspect(error)}") axn end end - defp deployment_status_ready?(status) do - status["replicas"] != nil && status["readyReplicas"] != nil && status["replicas"] == status["readyReplicas"] + defp application_status_ready?(status) do + replicas = Map.get(status, "replicas") + ready_replicas = Map.get(status, "readyReplicas") + suspended = Map.get(status, "suspended", true) + + cond do + not is_nil(replicas) and not is_nil(ready_replicas) -> + replicas == ready_replicas + + true -> + # For CronJobs: considered ready if not suspended + suspended == false + end end - defp deployment_status_asleep?(status) do - Map.get(status, "replicas", 0) == 0 && Map.get(status, "readyReplicas", 0) == 0 + defp application_status_asleep?(status) do + replicas = Map.get(status, "replicas", 0) + ready = Map.get(status, "readyReplicas", 0) + suspended = Map.get(status, "suspended", nil) + + cond do + not is_nil(suspended) -> + suspended == true + + true -> + replicas == 0 and ready == 0 + end end - defp check_deployment_status(axn, check_fn) do - case SleepSchedule.get_deployments(axn.resource) do - {:ok, deployments} -> - result = Enum.all?(deployments, fn deployment -> - Logger.debug("Deployment #{Deployment.name(deployment)} replicas: #{Deployment.replicas(deployment)}, readyReplicas: #{Deployment.ready_replicas(deployment)}") + defp check_application_status(axn, check_fn) do + with {:ok, deployments} <- SleepSchedule.get_deployments(axn.resource), + {:ok, statefulsets} <- SleepSchedule.get_statefulsets(axn.resource), + {:ok, cronjobs} <- SleepSchedule.get_cronjobs(axn.resource) do + + deployments_result = + Enum.all?(deployments, fn deployment -> + Logger.debug( + "Deployment #{Deployment.name(deployment)} replicas: #{Deployment.replicas(deployment)}, readyReplicas: #{Deployment.ready_replicas(deployment)}" + ) + check_fn.(deployment["status"]) end) - {:ok, result} - {:error, error} -> - {:error, error} + + statefulsets_result = + Enum.all?(statefulsets, fn statefulset -> + Logger.debug( + "StatefulSet #{StatefulSet.name(statefulset)} replicas: #{StatefulSet.replicas(statefulset)}, readyReplicas: #{StatefulSet.ready_replicas(statefulset)}" + ) + + check_fn.(statefulset["status"]) + end) + + cronjobs_result = + Enum.all?(cronjobs, fn cronjob -> + suspended = CronJob.suspend(cronjob) + + Logger.debug("CronJob #{CronJob.name(cronjob)} suspended: #{suspended}") + + # check_fn determines if suspended is the expected state (true for sleep, false for wake) + check_fn.(%{"suspended" => suspended}) + end) + + # Determine which resources we need to check based on what's present + has_deployments = Enum.any?(deployments) + has_statefulsets = Enum.any?(statefulsets) + has_cronjobs = Enum.any?(cronjobs) + + case {has_deployments, has_statefulsets, has_cronjobs} do + {false, false, false} -> {:ok, true} + {true, false, false} -> {:ok, deployments_result} + {false, true, false} -> {:ok, statefulsets_result} + {false, false, true} -> {:ok, cronjobs_result} + {true, true, false} -> {:ok, deployments_result && statefulsets_result} + {true, false, true} -> {:ok, deployments_result && cronjobs_result} + {false, true, true} -> {:ok, statefulsets_result && cronjobs_result} + {true, true, true} -> {:ok, deployments_result && statefulsets_result && cronjobs_result} + end + else + {:error, error} -> {:error, error} end end defp complete_sleep_transition(axn, opts) do Logger.info("Sleep transition complete") manual_override = Keyword.get(opts, :manual_override, false) - sleep_reason = if manual_override, do: "ManualSleep", else: "ScheduledSleep" + wake_time = axn.resource["spec"]["wakeTime"] + permanent_sleep = is_nil(wake_time) or wake_time == "" + + sleep_reason = cond do + manual_override -> "ManualSleep" + permanent_sleep -> "PermanentSleep" + true -> "ScheduledSleep" + end + + sleep_message = if permanent_sleep do + "Deployments, StatefulSets, and CronJobs have been scaled down/suspended indefinitely and ingress redirected." + else + "Deployments, StatefulSets, and CronJobs have been scaled down/suspended and ingress redirected." + end + axn |> set_condition("Transitioning", false, "NoTransition", "No transition in progress") - |> set_condition("Sleeping", true, sleep_reason, "Deployments have been scaled down and ingress redirected.") + |> set_condition("Sleeping", true, sleep_reason, sleep_message) |> set_condition("Error", false, "None", "No error") end @@ -230,21 +329,21 @@ defmodule Drowzee.Controller.SleepScheduleController do wake_reason = if manual_override, do: "ManualWakeUp", else: "ScheduledWakeUp" axn |> set_condition("Transitioning", false, "NoTransition", "No transition in progress") - |> set_condition("Sleeping", false, wake_reason, "Deployments have been scaled up and ingress restored.") + |> set_condition("Sleeping", false, wake_reason, "Deployments and StatefulSets have been scaled up, CronJobs have been resumed, and ingress restored.") |> set_condition("Error", false, "None", "No error") end defp put_ingress_to_sleep(axn) do case SleepSchedule.put_ingress_to_sleep(axn.resource) do {:ok, _} -> - Logger.info("Updated ingress to redirect to Drowzee", ingress_name: SleepSchedule.ingress_name(axn.resource)) + Logger.info("Updated ingress to redirect to Drowzee", name: SleepSchedule.ingress_name(axn.resource)) # register_event(axn, nil, :Normal, "SleepIngress", "Ingress has been redirected to Drowzee") axn {:error, :ingress_name_not_set} -> Logger.info("No ingressName has been provided so there is nothing to redirect") axn {:error, error} -> - Logger.error("Failed to redirect ingress to Drowzee: #{inspect(error)}", ingress_name: SleepSchedule.ingress_name(axn.resource)) + Logger.error("Failed to redirect ingress to Drowzee: #{inspect(error)}", name: SleepSchedule.ingress_name(axn.resource)) axn end end @@ -252,14 +351,14 @@ defmodule Drowzee.Controller.SleepScheduleController do defp wake_up_ingress(axn) do case SleepSchedule.wake_up_ingress(axn.resource) do {:ok, _} -> - Logger.info("Removed Drowzee redirect from ingress", ingress_name: SleepSchedule.ingress_name(axn.resource)) + Logger.info("Removed Drowzee redirect from ingress", name: SleepSchedule.ingress_name(axn.resource)) # register_event(axn, nil, :Normal, "WakeUpIngress", "Ingress has been restored") axn {:error, :ingress_name_not_set} -> Logger.info("No ingressName has been provided so there is nothing to restore") axn {:error, error} -> - Logger.error("Failed to remove Drowzee redirect from ingress: #{inspect(error)}", ingress_name: SleepSchedule.ingress_name(axn.resource)) + Logger.error("Failed to remove Drowzee redirect from ingress: #{inspect(error)}", name: SleepSchedule.ingress_name(axn.resource)) axn end end diff --git a/lib/drowzee/k8s.ex b/lib/drowzee/k8s.ex index 1b6ee28..f2f05f4 100644 --- a/lib/drowzee/k8s.ex +++ b/lib/drowzee/k8s.ex @@ -73,7 +73,7 @@ defmodule Drowzee.K8s do def get_drowzee_ingress() do name = "drowzee" namespace = drowzee_namespace() - Logger.debug("Fetching drowzee ingress", ingress_name: name, drowzee_namespace: namespace) + Logger.debug("Fetching drowzee ingress", name: name, drowzee_namespace: namespace) K8s.Client.get("networking.k8s.io/v1", :ingress, name: name, namespace: namespace) |> K8s.Client.put_conn(conn()) |> K8s.Client.run() @@ -90,9 +90,27 @@ defmodule Drowzee.K8s do end def get_deployment(name, namespace) do - Logger.debug("Fetching deployment", deployment_name: name) + Logger.debug("Fetching deployment", name: name) K8s.Client.get("apps/v1", :deployment, name: name, namespace: namespace) |> K8s.Client.put_conn(conn()) |> K8s.Client.run() end + + def get_statefulset(name, namespace) do + Logger.debug("Fetching statefulset", name: name) + K8s.Client.get("apps/v1", :statefulset, name: name, namespace: namespace) + |> K8s.Client.put_conn(conn()) + |> K8s.Client.run() + end + + def get_cronjob(name, namespace) do + Logger.debug("Fetching cronjob", name: name) + K8s.Client.get("batch/v1", :cronjob, name: name, namespace: namespace) + |> K8s.Client.put_conn(conn()) + |> K8s.Client.run() + end + + def update_sleep_schedule(sleep_schedule) do + Drowzee.K8s.SleepSchedule.update_sleep_schedule(sleep_schedule) + end end diff --git a/lib/drowzee/k8s/cronjob.ex b/lib/drowzee/k8s/cronjob.ex new file mode 100644 index 0000000..7cae0df --- /dev/null +++ b/lib/drowzee/k8s/cronjob.ex @@ -0,0 +1,20 @@ +defmodule Drowzee.K8s.CronJob do + require Logger + + def name(cronjob), do: cronjob["metadata"]["name"] + + def namespace(cronjob), do: cronjob["metadata"]["namespace"] + + def suspend(cronjob), do: cronjob["spec"]["suspend"] || false + + def suspend_cronjob(%{"kind" => "CronJob"} = cronjob, suspend) do + Logger.info("Setting cronjob suspend", name: name(cronjob), suspend: suspend) + cronjob = put_in(cronjob["spec"]["suspend"], suspend) + case K8s.Client.run(Drowzee.K8s.conn(), K8s.Client.update(cronjob)) do + {:ok, cronjob} -> {:ok, cronjob} + {:error, reason} -> + Logger.error("Failed to suspend cronjob: #{inspect(reason)}", name: name(cronjob), suspend: suspend) + {:error, reason} + end + end +end diff --git a/lib/drowzee/k8s/deployment.ex b/lib/drowzee/k8s/deployment.ex index 59bca33..a37a153 100644 --- a/lib/drowzee/k8s/deployment.ex +++ b/lib/drowzee/k8s/deployment.ex @@ -1,29 +1,58 @@ defmodule Drowzee.K8s.Deployment do require Logger - def name(deployment) do - deployment["metadata"]["name"] - end + def name(deployment), do: deployment["metadata"]["name"] - def namespace(deployment) do - deployment["metadata"]["namespace"] - end + def namespace(deployment), do: deployment["metadata"]["namespace"] + + def replicas(deployment), do: deployment["spec"]["replicas"] || 0 + + def ready_replicas(deployment), do: deployment["status"]["readyReplicas"] || 0 - def replicas(deployment) do - deployment["spec"]["replicas"] || 0 + @doc """ + Always save (or update) the current replica count as an annotation on the deployment. + """ + def save_original_replicas(deployment) do + annotations = get_in(deployment, ["metadata", "annotations"]) || %{} + current = get_in(deployment, ["spec", "replicas"]) || 0 + current_str = Integer.to_string(current) + + # Only save if: + # - annotation is missing OR + # - annotation value != current AND current > 0 + # This way, if someone changed the replica count while awake, you update it before sleep + case Map.get(annotations, "drowzee.io/original-replicas") do + nil when current > 0 -> + put_in(deployment, ["metadata", "annotations", "drowzee.io/original-replicas"], current_str) + value when value != current_str and current > 0 -> + put_in(deployment, ["metadata", "annotations", "drowzee.io/original-replicas"], current_str) + _ -> + deployment + end end - def ready_replicas(deployment) do - deployment["status"]["readyReplicas"] || 0 + @doc """ + Get the original replicas count from the annotation, as integer. Default to 1 if missing or invalid. + """ + def get_original_replicas(deployment) do + annotations = get_in(deployment, ["metadata", "annotations"]) || %{} + case Map.get(annotations, "drowzee.io/original-replicas") do + nil -> 1 + value -> + case Integer.parse(value) do + {count, _} -> count + :error -> 1 + end + end end def scale_deployment(%{"kind" => "Deployment"} = deployment, replicas) do - Logger.info("Scaling deployment", deployment: name(deployment), replicas: replicas) + Logger.info("Scaling deployment", name: name(deployment), replicas: replicas) deployment = put_in(deployment["spec"]["replicas"], replicas) case K8s.Client.run(Drowzee.K8s.conn(), K8s.Client.update(deployment)) do {:ok, deployment} -> {:ok, deployment} {:error, reason} -> - Logger.error("Failed to scale deployment: #{inspect(reason)}", deployment: name(deployment), replicas: replicas) + Logger.error("Failed to scale deployment: #{inspect(reason)}", name: name(deployment), replicas: replicas) {:error, reason} end end diff --git a/lib/drowzee/k8s/sleep_schedule.ex b/lib/drowzee/k8s/sleep_schedule.ex index 075c686..5d76266 100644 --- a/lib/drowzee/k8s/sleep_schedule.ex +++ b/lib/drowzee/k8s/sleep_schedule.ex @@ -3,7 +3,7 @@ defmodule Drowzee.K8s.SleepSchedule do require Logger - alias Drowzee.K8s.{Deployment, Ingress, Condition} + alias Drowzee.K8s.{Deployment, StatefulSet, CronJob, Ingress, Condition} def name(sleep_schedule) do sleep_schedule["metadata"]["name"] @@ -14,10 +14,23 @@ defmodule Drowzee.K8s.SleepSchedule do end def deployment_names(sleep_schedule) do + Logger.debug("Deployment entries: #{inspect(sleep_schedule["spec"]["deployments"])}") (sleep_schedule["spec"]["deployments"] || []) |> Enum.map(& &1["name"]) end + def statefulset_names(sleep_schedule) do + Logger.debug("Statefulsets entries: #{inspect(sleep_schedule["spec"]["statefulsets"])}") + (sleep_schedule["spec"]["statefulsets"] || []) + |> Enum.map(& &1["name"]) + end + + def cronjob_names(sleep_schedule) do + Logger.debug("CronJobs entries: #{inspect(sleep_schedule["spec"]["cronjobs"])}") + (sleep_schedule["spec"]["cronjobs"] || []) + |> Enum.map(& &1["name"]) + end + def ingress_name(sleep_schedule) do sleep_schedule["spec"]["ingressName"] end @@ -46,7 +59,7 @@ defmodule Drowzee.K8s.SleepSchedule do "" -> {:error, :ingress_name_not_set} ingress_name -> namespace = namespace(sleep_schedule) - Logger.debug("Fetching ingress", ingress_name: ingress_name) + Logger.debug("Fetching ingress", name: ingress_name) K8s.Client.get("networking.k8s.io/v1", :ingress, name: ingress_name, namespace: namespace) |> K8s.Client.put_conn(Drowzee.K8s.conn()) |> K8s.Client.run() @@ -66,11 +79,67 @@ defmodule Drowzee.K8s.SleepSchedule do end end + @retry with: exponential_backoff(1000) |> Stream.take(2) + def get_statefulsets(sleep_schedule) do + namespace = namespace(sleep_schedule) + results = (statefulset_names(sleep_schedule) || []) + |> Stream.map(&Drowzee.K8s.get_statefulset(&1, namespace)) + |> Enum.to_list() + + case Enum.all?(results, fn {:ok, _} -> true; _ -> false end) do + true -> {:ok, Enum.map(results, fn {:ok, statefulset} -> statefulset end)} + false -> {:error, Enum.filter(results, fn {:error, _} -> true; _ -> false end) |> Enum.map(fn {:error, error} -> error end)} + end + end + + @retry with: exponential_backoff(1000) |> Stream.take(2) + def get_cronjobs(sleep_schedule) do + namespace = namespace(sleep_schedule) + results = (cronjob_names(sleep_schedule) || []) + |> Stream.map(&Drowzee.K8s.get_cronjob(&1, namespace)) + |> Enum.to_list() + + case Enum.all?(results, fn {:ok, _} -> true; _ -> false end) do + true -> {:ok, Enum.map(results, fn {:ok, cronjob} -> cronjob end)} + false -> {:error, Enum.filter(results, fn {:error, _} -> true; _ -> false end) |> Enum.map(fn {:error, error} -> error end)} + end + end + def scale_down_deployments(sleep_schedule) do Logger.debug("Scaling down deployments...") case get_deployments(sleep_schedule) do {:ok, deployments} -> - results = Enum.map(deployments, &Deployment.scale_deployment(&1, 0)) + results = + Enum.map(deployments, fn deployment -> + deployment = Deployment.save_original_replicas(deployment) + Deployment.scale_deployment(deployment, 0) + end) + {:ok, results} + {:error, error} -> + {:error, error} + end + end + + def scale_down_statefulsets(sleep_schedule) do + Logger.debug("Scaling down statefulsets...") + case get_statefulsets(sleep_schedule) do + {:ok, statefulsets} -> + results = + Enum.map(statefulsets, fn statefulset -> + statefulset = StatefulSet.save_original_replicas(statefulset) + StatefulSet.scale_statefulset(statefulset, 0) + end) + {:ok, results} + {:error, error} -> + {:error, error} + end + end + + def suspend_cronjobs(sleep_schedule) do + Logger.debug("Suspending cronjobs...") + case get_cronjobs(sleep_schedule) do + {:ok, cronjobs} -> + results = Enum.map(cronjobs, &CronJob.suspend_cronjob(&1, true)) {:ok, results} {:error, error} -> {:error, error} @@ -81,7 +150,36 @@ defmodule Drowzee.K8s.SleepSchedule do Logger.debug("Scaling up deployments...") case get_deployments(sleep_schedule) do {:ok, deployments} -> - results = Enum.map(deployments, &Deployment.scale_deployment(&1, 1)) + results = Enum.map(deployments, fn deployment -> + original = Deployment.get_original_replicas(deployment) + Deployment.scale_deployment(deployment, original) + end) + {:ok, results} + {:error, error} -> + {:error, error} + end + end + + def scale_up_statefulsets(sleep_schedule) do + Logger.debug("Scaling up statefulsets...") + case get_statefulsets(sleep_schedule) do + {:ok, statefulsets} -> + results = + Enum.map(statefulsets, fn statefulset -> + original = StatefulSet.get_original_replicas(statefulset) + StatefulSet.scale_statefulset(statefulset, original) + end) + {:ok, results} + {:error, error} -> + {:error, error} + end + end + + def resume_cronjobs(sleep_schedule) do + Logger.debug("Resuming cronjobs...") + case get_cronjobs(sleep_schedule) do + {:ok, cronjobs} -> + results = Enum.map(cronjobs, &CronJob.suspend_cronjob(&1, false)) {:ok, results} {:error, error} -> {:error, error} @@ -111,6 +209,14 @@ defmodule Drowzee.K8s.SleepSchedule do end end + def update_sleep_schedule(sleep_schedule) do + require Logger + Logger.info("Updating SleepSchedule #{sleep_schedule["metadata"]["name"]} in namespace #{sleep_schedule["metadata"]["namespace"]}") + result = K8s.Client.run(Drowzee.K8s.conn(), K8s.Client.update(sleep_schedule)) + Logger.debug("Update result: #{inspect(result)}") + result + end + def update_heartbeat(sleep_schedule) do Logger.debug("Update heartbeat...") heartbeat = get_condition(sleep_schedule, "Heartbeat") || %{ "status" => "False" } diff --git a/lib/drowzee/k8s/statefulset.ex b/lib/drowzee/k8s/statefulset.ex new file mode 100644 index 0000000..cbaf15b --- /dev/null +++ b/lib/drowzee/k8s/statefulset.ex @@ -0,0 +1,59 @@ +defmodule Drowzee.K8s.StatefulSet do + require Logger + + def name(statefulset), do: statefulset["metadata"]["name"] + + def namespace(statefulset), do: statefulset["metadata"]["namespace"] + + def replicas(statefulset), do: statefulset["spec"]["replicas"] || 0 + + def ready_replicas(statefulset), do: statefulset["status"]["readyReplicas"] || 0 + + @doc """ + Always save (or update) the current replica count as an annotation on the statefulset. + """ + def save_original_replicas(statefulset) do + annotations = get_in(statefulset, ["metadata", "annotations"]) || %{} + current = get_in(statefulset, ["spec", "replicas"]) || 0 + current_str = Integer.to_string(current) + + # Only save if: + # - annotation is missing OR + # - annotation value != current AND current > 0 + # This way, if someone changed the replica count while awake, you update it before sleep + case Map.get(annotations, "drowzee.io/original-replicas") do + nil when current > 0 -> + put_in(statefulset, ["metadata", "annotations", "drowzee.io/original-replicas"], current_str) + value when value != current_str and current > 0 -> + put_in(statefulset, ["metadata", "annotations", "drowzee.io/original-replicas"], current_str) + _ -> + statefulset + end + end + + @doc """ + Get the original replicas count from the annotation, as integer. Default to 1 if missing or invalid. + """ + def get_original_replicas(statefulset) do + annotations = get_in(statefulset, ["metadata", "annotations"]) || %{} + case Map.get(annotations, "drowzee.io/original-replicas") do + nil -> 1 + value -> + case Integer.parse(value) do + {count, _} -> count + :error -> 1 + end + end + end + + def scale_statefulset(%{"kind" => "StatefulSet"} = statefulset, replicas) do + Logger.info("Scaling statefulset", name: name(statefulset), replicas: replicas) + statefulset = put_in(statefulset["spec"]["replicas"], replicas) + case K8s.Client.run(Drowzee.K8s.conn(), K8s.Client.update(statefulset)) do + {:ok, statefulset} -> {:ok, statefulset} + {:error, reason} -> + Logger.error("Failed to scale statefulset: #{inspect(reason)}", name: name(statefulset), replicas: replicas) + {:error, reason} + end + end +end diff --git a/lib/drowzee/operator.ex b/lib/drowzee/operator.ex index 19574d3..67175ed 100644 --- a/lib/drowzee/operator.ex +++ b/lib/drowzee/operator.ex @@ -21,12 +21,24 @@ defmodule Drowzee.Operator do namespaces = Drowzee.Config.namespaces() # Note: To watch all namespaces set BONNY_POD_NAMESPACE to "__ALL__" Logger.info("Configuring SleepScheduleController controller with namespace(s): #{Enum.join(namespaces, ", ")}") - Enum.map(namespaces, fn namespace -> - %{ - query: K8s.Client.watch("drowzee.challengr.io/v1beta1", "SleepSchedule", namespace: namespace), - controller: Drowzee.Controller.SleepScheduleController - } - end) + + if Enum.member?(namespaces, :all) do + # If :all is in the list, watch all namespaces + [ + %{ + query: K8s.Client.watch("drowzee.challengr.io/v1beta1", "SleepSchedule", namespace: :all), + controller: Drowzee.Controller.SleepScheduleController + } + ] + else + # Otherwise watch the specific namespaces + Enum.map(namespaces, fn namespace -> + %{ + query: K8s.Client.watch("drowzee.challengr.io/v1beta1", "SleepSchedule", namespace: namespace), + controller: Drowzee.Controller.SleepScheduleController + } + end) + end end @impl Bonny.Operator diff --git a/lib/drowzee/sleep_checker.ex b/lib/drowzee/sleep_checker.ex index 7226c4f..f40adf9 100644 --- a/lib/drowzee/sleep_checker.ex +++ b/lib/drowzee/sleep_checker.ex @@ -1,6 +1,17 @@ defmodule Drowzee.SleepChecker do require Logger + def naptime?(_sleep_time, nil, _timezone) do + # If wake_time is nil, always consider it naptime (always sleeping) + Logger.debug("Wake time is not defined, resources will remain scaled down/suspended indefinitely") + {:ok, true} + end + + def naptime?(sleep_time, "", timezone) do + # Empty string wake_time is treated the same as nil + naptime?(sleep_time, nil, timezone) + end + def naptime?(sleep_time, wake_time, timezone) do now = DateTime.now!(timezone) today_date = DateTime.to_date(now) diff --git a/lib/drowzee/transition_monitor.ex b/lib/drowzee/transition_monitor.ex index 1db000a..b182af9 100644 --- a/lib/drowzee/transition_monitor.ex +++ b/lib/drowzee/transition_monitor.ex @@ -17,6 +17,8 @@ defmodule Drowzee.TransitionMonitor do Bonny.PeriodicTask.new(task_name, {Drowzee.TransitionMonitor, :monitor_transition, [name, namespace, 1]}, 5000) end + @spec monitor_transition(binary(), :all | binary(), any()) :: + {:ok, [:all | binary() | number(), ...]} | {:stop, <<_::152, _::_*192>>} def monitor_transition(name, namespace, attempt) do sleep_schedule = Drowzee.K8s.get_sleep_schedule!(name, namespace) case {Drowzee.K8s.SleepSchedule.get_condition(sleep_schedule, "Transitioning"), attempt} do diff --git a/lib/drowzee/v1beta1/sleep_schedule.ex b/lib/drowzee/v1beta1/sleep_schedule.ex index 03c9d68..075b4a1 100644 --- a/lib/drowzee/v1beta1/sleep_schedule.ex +++ b/lib/drowzee/v1beta1/sleep_schedule.ex @@ -51,6 +51,11 @@ defmodule Drowzee.API.V1Beta1.SleepSchedule do spec: %{ type: :object, properties: %{ + enabled: %{ + type: :boolean, + description: "Whether this schedule is enabled.", + default: true + }, deployments: %{ description: "The deployments that will be slept/woken.", type: :array, @@ -87,10 +92,13 @@ defmodule Drowzee.API.V1Beta1.SleepSchedule do } }, additionalPrinterColumns: [ + %{name: "Enabled", type: :boolean, description: "Enabled", jsonPath: ".spec.enabled"}, %{name: "SleepTime", type: :string, description: "Starts Sleeping", jsonPath: ".spec.sleepTime"}, %{name: "WakeTime", type: :string, description: "Wakes Up", jsonPath: ".spec.wakeTime"}, %{name: "Timezone", type: :string, description: "Timezone", jsonPath: ".spec.timezone"}, %{name: "Deployments", type: :string, description: "Deployments", jsonPath: ".spec.deployments[*].name"}, + %{name: "Statefulsets", type: :string, description: "Statefulsets", jsonPath: ".spec.statefulsets[*].name"}, + %{name: "Cronjobs", type: :string, description: "CronJobs", jsonPath: ".spec.cronjobs[*].name"}, %{name: "Sleeping?", type: :string, description: "Current Status", jsonPath: ".status.conditions[?(@.type == \"Sleeping\")].status"}, %{name: "Transitioning?", type: :string, description: "Status Change In Progress", jsonPath: ".status.conditions[?(@.type == \"Transitioning\")].status"}, %{name: "ManualOverride?", type: :string, description: "Status overridden by user", jsonPath: ".status.conditions[?(@.type == \"ManualOverride\")].status"} diff --git a/lib/drowzee_web/live/home_live/index.ex b/lib/drowzee_web/live/home_live/index.ex index 666e86a..9638907 100644 --- a/lib/drowzee_web/live/home_live/index.ex +++ b/lib/drowzee_web/live/home_live/index.ex @@ -99,6 +99,21 @@ defmodule DrowzeeWeb.HomeLive.Index do {:noreply, socket} end + @impl true + def handle_event("toggle_enabled", %{"name" => name, "namespace" => namespace}, socket) do + sleep_schedule = Drowzee.K8s.get_sleep_schedule!(name, namespace) + new_enabled = not Map.get(sleep_schedule["spec"], "enabled", true) + updated_sleep_schedule = + put_in(sleep_schedule, ["spec", "enabled"], new_enabled) + |> Drowzee.K8s.update_sleep_schedule() + |> case do + {:ok, s} -> s + {:error, _} -> sleep_schedule + end + socket = replace_sleep_schedule(socket, updated_sleep_schedule) + {:noreply, socket} + end + @impl true def handle_event("sleep_all_schedules", %{"namespace" => namespace}, socket) when is_binary(namespace) do sleep_schedules = Drowzee.K8s.sleep_schedules(namespace) @@ -184,9 +199,48 @@ defmodule DrowzeeWeb.HomeLive.Index do [Drowzee.K8s.get_sleep_schedule!(name, socket.assigns.namespace)] end + {deployments_by_name, statefulsets_by_name, cronjobs_by_name} = + case socket.assigns.name do + nil -> {%{}, %{}, %{}} + _name -> + deployments = + sleep_schedules + |> Enum.flat_map(fn schedule -> + case Drowzee.K8s.SleepSchedule.get_deployments(schedule) do + {:ok, ds} -> ds + {:error, _} -> [] + end + end) + statefulsets = + sleep_schedules + |> Enum.flat_map(fn schedule -> + case Drowzee.K8s.SleepSchedule.get_statefulsets(schedule) do + {:ok, ss} -> ss + {:error, _} -> [] + end + end) + cronjobs = + sleep_schedules + |> Enum.flat_map(fn schedule -> + case Drowzee.K8s.SleepSchedule.get_cronjobs(schedule) do + {:ok, cs} -> cs + {:error, _} -> [] + end + end) + { + Map.new(deployments, &{&1["metadata"]["name"], &1}), + Map.new(statefulsets, &{&1["metadata"]["name"], &1}), + Map.new(cronjobs, &{&1["metadata"]["name"], &1}) + } + end + socket |> assign(:sleep_schedules, sleep_schedules) + |> assign(:deployments_by_name, deployments_by_name) + |> assign(:statefulsets_by_name, statefulsets_by_name) + |> assign(:cronjobs_by_name, cronjobs_by_name) |> filter_sleep_schedules(socket.assigns.search) + end def sleep_schedule_host(sleep_schedule) do diff --git a/lib/drowzee_web/live/home_live/index.html.heex b/lib/drowzee_web/live/home_live/index.html.heex index b637787..101880f 100644 --- a/lib/drowzee_web/live/home_live/index.html.heex +++ b/lib/drowzee_web/live/home_live/index.html.heex @@ -1,16 +1,68 @@ -
-
+
+ +
+
+ + +
+
+ + +
+
<.link navigate={~p"/"} class="flex items-center"> Logo -

Drowzee

+

Drowzee

- <.form - for={%{}} - phx-change="search" - phx-submit="search" - class="ml-auto mr-2" - > + <.form for={%{}} phx-change="search" phx-submit="search" class="ml-auto mr-2"> <.input type="text" name="search" @@ -23,162 +75,284 @@
-
- <%= if @namespace == nil do %> -

All namespaces

- <% else %> -
-

- <.link navigate={~p"/#{@namespace}"}> - <%= String.capitalize(@namespace) %> Namespace - - <%= if @name != nil do %> - / <%= String.capitalize(@name) %> - <% end %> -

- <%= if @sleep_schedules != [] do %> -
- <.link phx-click="sleep_all_schedules" - phx-value-namespace={@namespace} - data-confirm={"Are you sure you want to sleep all schedules in the #{@namespace} namespace?"} - class="px-4 py-2 text-sm rounded-lg bg-blue-500 text-white hover:bg-blue-600 disabled:bg-blue-300 disabled:cursor-not-allowed"> - 😴 Sleep All - - <.link phx-click="wake_all_schedules" - phx-value-namespace={@namespace} - data-confirm={"Are you sure you want to wake all schedules in the #{@namespace} namespace?"} - class="px-4 py-2 text-sm rounded-lg bg-green-500 text-white hover:bg-green-600 disabled:bg-green-300 disabled:cursor-not-allowed"> - ☕️ Wake All - -
- <% end %> -
- <% end %> -
- -
    - <%= if @sleep_schedules == [] do %> -
  • -
    -

    - No sleep schedules found -

    -
    -
  • - <% end %> - - <%= for sleep_schedule <- @filtered_sleep_schedules || @sleep_schedules do %> -
  • -
    -

    - <%= if get_condition(sleep_schedule, "Sleeping")["status"] == "True" do %> - 🛌 - <% else %> - 😃 - <% end %> - <%= if @namespace == nil do %> - <.link navigate={~p"/#{sleep_schedule["metadata"]["namespace"]}"}> - <%= String.capitalize(sleep_schedule["metadata"]["namespace"]) %> - / - <% end %> - <.link navigate={~p"/#{sleep_schedule["metadata"]["namespace"]}/#{sleep_schedule["metadata"]["name"]}"}> - <%= String.capitalize(sleep_schedule["metadata"]["name"]) %> - - <%= if sleep_schedule_host(sleep_schedule) != nil do %> - <.link href={"http://#{sleep_schedule_host(sleep_schedule)}"} target="_blank"> - <.icon name="hero-arrow-top-right-on-square" class="inline-block w-6 h-6 ml-1 mb-2 text-gray-800" /> - +
    + <%= if @namespace == nil do %> +

    All namespaces

    + <% else %> +
    +

    + <.link navigate={~p"/#{@namespace}"}> + {String.capitalize(@namespace)} Namespace + + <%= if @name != nil do %> + / {String.capitalize(@name)} <% end %>

    - <%= if get_condition(sleep_schedule, "Sleeping")["status"] == "True" do %> - <%= if get_condition(sleep_schedule, "Transitioning")["status"] == "True" do %> - - <% else %> + <%= if @sleep_schedules != [] do %> +
    <.link - phx-click="wake_schedule" - phx-value-name={sleep_schedule["metadata"]["name"]} - phx-value-namespace={sleep_schedule["metadata"]["namespace"]} - class="px-4 py-2 text-sm rounded-lg bg-green-500 text-white hover:bg-green-600 disabled:bg-green-300 disabled:cursor-not-allowed" + phx-click="sleep_all_schedules" + phx-value-namespace={@namespace} + data-confirm={"Are you sure you want to sleep all schedules in the #{@namespace} namespace?"} + class="px-4 py-2 text-sm rounded-lg bg-blue-500 text-white hover:bg-blue-600 disabled:bg-blue-300 disabled:cursor-not-allowed" > - ☕️ Wake Up + 😴 Sleep All - <% end %> - <% else %> - <%= if get_condition(sleep_schedule, "Transitioning")["status"] == "True" do %> - - <% else %> <.link - phx-click="sleep_schedule" - phx-value-name={sleep_schedule["metadata"]["name"]} - phx-value-namespace={sleep_schedule["metadata"]["namespace"]} - class="px-4 py-2 text-sm rounded-lg bg-blue-500 text-white hover:bg-blue-600 disabled:bg-blue-300 disabled:cursor-not-allowed" + phx-click="wake_all_schedules" + phx-value-namespace={@namespace} + data-confirm={"Are you sure you want to wake all schedules in the #{@namespace} namespace?"} + class="px-4 py-2 text-sm rounded-lg bg-green-500 text-white hover:bg-green-600 disabled:bg-green-300 disabled:cursor-not-allowed" > - 💤 Sleep + ☕️ Wake All - <% end %> +
    <% end %>
    + <% end %> +
    -
    - 🕒 Schedule: - <%= sleep_schedule["spec"]["sleepTime"] %> - <%= sleep_schedule["spec"]["wakeTime"] %> (<%= sleep_schedule["spec"]["timezone"] %>) -
    + + +
      + <%= if @sleep_schedules == [] do %> +
    • +
      +

      + No sleep schedules found +

      +
      +
    • + <% end %> + + <%= for sleep_schedule <- @filtered_sleep_schedules || @sleep_schedules do %> +
    • +
      +

      + <%= if sleep_schedule["spec"]["enabled"] == false do %> + 🛑 + <% else %> + <%= if get_condition(sleep_schedule, "Sleeping")["status"] == "True" do %> + 🛌 + <% else %> + 😃 + <% end %> + <% end %> + <%= if @namespace == nil do %> + <.link navigate={~p"/#{sleep_schedule["metadata"]["namespace"]}"}> + {String.capitalize(sleep_schedule["metadata"]["namespace"])} + + / + <% end %> + <.link navigate={ + ~p"/#{sleep_schedule["metadata"]["namespace"]}/#{sleep_schedule["metadata"]["name"]}" + }> + {String.capitalize(sleep_schedule["metadata"]["name"])} + + <%= if sleep_schedule_host(sleep_schedule) != nil do %> + <.link href={"http://#{sleep_schedule_host(sleep_schedule)}"} target="_blank"> + <.icon + name="hero-arrow-top-right-on-square" + class="inline-block w-6 h-6 ml-1 mb-2 text-gray-800" + /> + + <% end %> +

      +
      + <%= if get_condition(sleep_schedule, "Sleeping")["status"] == "True" do %> + <%= if get_condition(sleep_schedule, "Transitioning")["status"] == "True" do %> + + <% else %> + <.link + phx-click="wake_schedule" + phx-value-name={sleep_schedule["metadata"]["name"]} + phx-value-namespace={sleep_schedule["metadata"]["namespace"]} + class="px-4 py-2 text-sm rounded-lg bg-green-500 text-white hover:bg-green-600 disabled:bg-green-300 disabled:cursor-not-allowed" + > + ☕️ Wake Up + + <% end %> + <% else %> + <%= if get_condition(sleep_schedule, "Transitioning")["status"] == "True" do %> + + <% else %> + <.link + phx-click="sleep_schedule" + phx-value-name={sleep_schedule["metadata"]["name"]} + phx-value-namespace={sleep_schedule["metadata"]["namespace"]} + class="px-4 py-2 text-sm rounded-lg bg-blue-500 text-white hover:bg-blue-600 disabled:bg-blue-300 disabled:cursor-not-allowed" + > + 💤 Sleep + + <% end %> + <% end %> <.link - phx-click="remove_override" + phx-click="toggle_enabled" phx-value-name={sleep_schedule["metadata"]["name"]} phx-value-namespace={sleep_schedule["metadata"]["namespace"]} - class="px-4 py-2 text-sm text-gray-600 hover:underline disabled:text-gray-300 disabled:cursor-not-allowed" - phx-disable-with="removing..." + class={ + if sleep_schedule["spec"]["enabled"] == false do + "px-4 py-2 text-sm rounded-lg bg-green-500 text-white hover:bg-green-600" + else + "px-4 py-2 text-sm rounded-lg bg-red-500 text-white hover:bg-red-600" + end + } > - + <%= if sleep_schedule["spec"]["enabled"] == false do %> + ▷ Enable + <% else %> + || Disable + <% end %> +
      +
      + +
      + 🕒 Schedule: + + {sleep_schedule["spec"]["sleepTime"]} - {sleep_schedule["spec"]["wakeTime"]} ({sleep_schedule[ + "spec" + ]["timezone"]})
      - <% end %> -
      -

      📦 Deployments:

      -
        - <%= for deployment <- sleep_schedule["spec"]["deployments"] do %> -
      • - 🚀 <%= deployment["name"] %> -
      • - <% end %> -
      -
      + <%= if get_condition(sleep_schedule, "ManualOverride")["status"] == "True" do %> +
      + 🛑 Manual override: + + {case get_condition(sleep_schedule, "ManualOverride")["reason"] do + "Sleep" -> "Put to sleep " + "WakeUp" -> "Woken up " + _ -> "Manual override" + end} at {last_transaction_time(sleep_schedule, "ManualOverride")} ({sleep_schedule[ + "spec" + ]["timezone"]}) + + + <.link + phx-click="remove_override" + phx-value-name={sleep_schedule["metadata"]["name"]} + phx-value-namespace={sleep_schedule["metadata"]["namespace"]} + class="px-4 py-2 text-sm text-gray-600 hover:underline disabled:text-gray-300 disabled:cursor-not-allowed" + phx-disable-with="removing..." + > + + + +
      + <% end %> + + <%= if sleep_schedule["spec"]["deployments"] != [] do %> +
      +

      📦 Deployments:

      +
        + <%= for deployment <- sleep_schedule["spec"]["deployments"] do %> +
      • + <% d = @deployments_by_name[deployment["name"]] %> + <%= if @name do %> + <%= if d && (Map.get(d["spec"], "replicas", 0) > 0) do %> + 🚀 + <% else %> + 💤 + <% end %> + <% end %> + {deployment["name"]} +
      • + <% end %> +
      +
      + <% end %> - <%= if get_condition(sleep_schedule, "Error")["status"] == "True" do %> -
      -
      -
      -
      - 🚨 Error -
      -
      - <%= last_transaction_time(sleep_schedule, "Error") %> + <%= if (sleep_schedule["spec"]["statefulsets"] || []) != [] do %> +
      +

      📦 StatefulSets:

      +
        + <%= for statefulset <- sleep_schedule["spec"]["statefulsets"] || [] do %> +
      • + <% s = @statefulsets_by_name[statefulset["name"]] %> + <%= if @name do %> + <%= if s && (Map.get(s["spec"], "replicas", 0) > 0) do %> + 🚀 + <% else %> + 💤 + <% end %> + <% end %> + {statefulset["name"]} +
      • + <% end %> +
      +
      + <% end %> + + <%= if (sleep_schedule["spec"]["cronjobs"] || []) != [] do %> +
      +

      📦 CronJobs:

      +
        + <%= for cronjob <- sleep_schedule["spec"]["cronjobs"] || [] do %> +
      • + <% c = @cronjobs_by_name[cronjob["name"]] %> + <%= if @name do %> + <%= if c && Map.get(c["spec"], "suspend", false) == false do %> + 🚀 + <% else %> + 💤 + <% end %> + <% end %> + {cronjob["name"]} +
      • + <% end %> +
      +
      + <% end %> + + <%= if get_condition(sleep_schedule, "Error")["status"] == "True" do %> +
      +
      +
      +
      + 🚨 Error +
      +
      + {last_transaction_time(sleep_schedule, "Error")} +
      +
      +
      + {get_condition(sleep_schedule, "Error")["message"]}
      -
      -
      - <%= get_condition(sleep_schedule, "Error")["message"] %>
      -
      - <% end %> -
    • - <% end %> -
    -

    \ No newline at end of file + <% end %> +
  • + <% end %> +
+
+
diff --git a/manifest.yaml b/manifest.yaml index 2ea578c..1b0b030 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -34,8 +34,8 @@ spec: status: type: string enum: - - 'True' - - 'False' + - "True" + - "False" type: type: string reason: @@ -71,13 +71,13 @@ spec: description: The ingress that will be slept/woken. sleepTime: type: string - description: 'The time that the deployment will start sleeping(format: HH:MMam/pm)' + description: "The time that the deployment will start sleeping(format: HH:MMam/pm)" timezone: type: string description: The timezone that the input times are based in wakeTime: type: string - description: 'The time that the deployment will wake up (format: HH:MMam/pm)' + description: "The time that the deployment will wake up (format: HH:MMam/pm)" storage: true additionalPrinterColumns: - name: SleepTime @@ -130,50 +130,50 @@ rules: apiGroups: - apiextensions.k8s.io verbs: - - '*' + - "*" - resources: - leases apiGroups: - coordination.k8s.io verbs: - - '*' + - "*" - resources: - sleepschedules apiGroups: - drowzee.challengr.io verbs: - - '*' + - "*" - resources: - sleepschedules/status apiGroups: - drowzee.challengr.io verbs: - - '*' + - "*" - resources: - events apiGroups: - events.k8s.io verbs: - - '*' + - "*" - resources: - deployments apiGroups: - "apps" verbs: - - '*' + - "*" - resources: - ingresses apiGroups: - networking.k8s.io verbs: - - '*' + - "*" - resources: - services - - configmaps + - configmaps apiGroups: - "" verbs: - - '*' + - "*" --- metadata: diff --git a/mix.lock b/mix.lock index db58d77..3735a64 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ - "bandit": {:hex, :bandit, "1.6.7", "42f30e37a1c89a2a12943c5dca76f731a2313e8a2e21c1a95dc8241893e922d1", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "551ba8ff5e4fc908cbeb8c9f0697775fb6813a96d9de5f7fe02e34e76fd7d184"}, + "bandit": {:hex, :bandit, "1.6.11", "2fbadd60c95310eefb4ba7f1e58810aa8956e18c664a3b2029d57edb7d28d410", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "543f3f06b4721619a1220bed743aa77bf7ecc9c093ba9fab9229ff6b99eacc65"}, "bonny": {:hex, :bonny, "1.4.0", "318e8fd23129e7c1b1e2d949fa29d75cff348d0cc3330f1dc902f24f0a7c429c", [:mix], [{:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:k8s, "~> 2.0", [hex: :k8s, repo: "hexpm", optional: false]}, {:owl, "~> 0.8.0", [hex: :owl, repo: "hexpm", optional: false]}, {:pluggable, "~> 1.0", [hex: :pluggable, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "91b3ba068c3724ab9abc95e053c41edf0f9990d1cd841b91cc494521fda6a756"}, - "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"}, + "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, @@ -9,12 +9,12 @@ "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, - "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"}, + "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, - "hackney": {:hex, :hackney, "1.22.0", "4efc68df70322d4d2e3d2744e9bd191a39a0cb8d08c35379a08d9fb0f040d595", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "628569e451820950382be3d3e6481d7c59997e606c7823bddb4ce5d10812dfcb"}, + "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, - "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "igniter": {:hex, :igniter, "0.5.15", "422c7f25340c8846eaad6c0f63b72dfd74019829130fca5bf75ad5f3716e97ec", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "dcea787ddb5519a6bdb1799b373fabe553e5ffcefc17c85688e7bbbceee25a97"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, @@ -29,15 +29,15 @@ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "owl": {:hex, :owl, "0.8.0", "0ef925cb784311093d4e3734822960cbdbdb13b095d748bb5bc82abcd5b56732", [:mix], [], "hexpm", "0a5586ceb1a12f4bbda90e330c20e6ea034552335d09466c10e4218c98977529"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "phoenix": {:hex, :phoenix, "1.7.19", "36617efe5afbd821099a8b994ff4618a340a5bfb25531a1802c4d4c634017a57", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ba4dc14458278773f905f8ae6c2ec743d52c3a35b6b353733f64f02dfe096cd6"}, - "phoenix_html": {:hex, :phoenix_html, "4.2.0", "83a4d351b66f472ebcce242e4ae48af1b781866f00ef0eb34c15030d4e2069ac", [:mix], [], "hexpm", "9713b3f238d07043583a94296cc4bbdceacd3b3a6c74667f4df13971e7866ec8"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.4", "327491b033e79db2f887b065c5a2993228449091883d74cfa1baa12f8c98d5eb", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a9865316ddf8d78f382d63af278d20436b52d262b60239956817a61279514366"}, + "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, + "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.11", "76f6416cb31e2e49601c62b79edc9e6fbd0706368bba9f49c4bd13934b785a28", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "522b8c164d1a0009d30fd3364538d17684cb6f8e6a6931f511f82d891c634cdd"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, - "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "pluggable": {:hex, :pluggable, "1.1.0", "7eba3bc70c0caf4d9056c63c882df8862f7534f0145da7ab3a47ca73e4adb1e4", [:mix], [], "hexpm", "d12eb00ea47b21e92cd2700d6fbe3737f04b64e71b63aad1c0accde87c751637"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"}, @@ -46,14 +46,14 @@ "sourceror": {:hex, :sourceror, "1.9.0", "3bf5fe2d017aaabe3866d8a6da097dd7c331e0d2d54e59e21c2b066d47f1e08e", [:mix], [], "hexpm", "d20a9dd5efe162f0d75a307146faa2e17b823ea4f134f662358d70f0332fed82"}, "spitfire": {:hex, :spitfire, "0.2.0", "0de1f519a23f65bde40d316adad53c07a9563f25cc68915d639d8a509a0aad8a", [:mix], [], "hexpm", "743daaee2d81a0d8095431729f478ce49b47ea8943c7d770de86704975cb7775"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, + "tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, - "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, - "thousand_island": {:hex, :thousand_island, "1.3.10", "a9971ebab1dfb36e2710a86b37c3f54973fbc9470d892035334415521fb53328", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17ab1f1b13aadb1f4b4c8e5b59c06874d701119fed082884c9c6d38addad254f"}, + "thousand_island": {:hex, :thousand_island, "1.3.13", "d598c609172275f7b1648c9f6eddf900e42312b09bfc2f2020358f926ee00d39", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a34bdf24ae2f965ddf7ba1a416f3111cfe7df50de8d66f6310e01fc2e80b02a"}, "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, - "tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"}, + "tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, diff --git a/sleep_schedule.yaml b/sleep_schedule.yaml index a226e04..825d6fe 100644 --- a/sleep_schedule.yaml +++ b/sleep_schedule.yaml @@ -3,9 +3,9 @@ kind: SleepSchedule metadata: name: testing spec: - sleepTime: '10:00pm' - wakeTime: '09:00am' - timezone: 'Australia/Sydney' + sleepTime: "10:00pm" + wakeTime: "09:00am" + timezone: "Australia/Sydney" deployments: - name: dummy-service ingressName: dummy-service @@ -16,9 +16,9 @@ metadata: name: service1 namespace: services spec: - sleepTime: '10:00pm' - wakeTime: '09:00am' - timezone: 'Australia/Sydney' + sleepTime: "10:00pm" + wakeTime: "09:00am" + timezone: "Australia/Sydney" deployments: - name: service1 - name: service1-worker @@ -30,10 +30,27 @@ metadata: name: service2 namespace: services spec: - sleepTime: '10:00pm' - wakeTime: '09:00am' - timezone: 'Australia/Sydney' + sleepTime: "10:00pm" + wakeTime: "09:00am" + timezone: "Australia/Sydney" deployments: - name: service2 - name: service2-worker ingressName: service2 +--- +apiVersion: drowzee.challengr.io/v1beta1 +kind: SleepSchedule +metadata: + name: permanent-down + namespace: services +spec: + sleepTime: "08:00pm" + # No wakeTime defined - resources will remain scaled down indefinitely + timezone: "Australia/Sydney" + deployments: + - name: non-prod-service + statefulsets: + - name: non-prod-database + cronjobs: + - name: non-prod-backup + ingressName: non-prod-service