diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..f0711ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,53 @@ +name: Bug Report +description: Use this template when creating a bug report. +labels: +- bug + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + - type: textarea + id: project-version + attributes: + label: Project version + description: What version of this project are you using? + validations: + required: true + + - type: textarea + id: describe-bug + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: reproduction-steps + attributes: + label: Reproduction steps + description: Steps to reproduce the behavior + value: | + 1. + 2. + 3. + ... + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000..b2e4e293 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,37 @@ +name: Feature Request +description: Use this template when creating a feature request. +labels: +- enhancement + +body: + - type: textarea + id: describe-problem + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. + validations: + required: true + + - type: textarea + id: describe-solution + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: describe-alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.yml b/.github/PULL_REQUEST_TEMPLATE.yml new file mode 100644 index 00000000..b033d7be --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.yml @@ -0,0 +1,40 @@ +name: Pull Request +description: Use this template when creating a pull request. + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to create this pull request. + + Please note that as per our contribution guidelines, unless the pull + request is for a simple typo correction or obvious bug fix, it's + recommended to initiate a discussion by first raising an issue. This + enables us to assess proposed modifications and ascertain whether they + necessitate adjustments to the core platform or if there's a more + effective approach to achieve the desired outcome without making + modifications. + + - type: textarea + id: related-issues + attributes: + label: Related issue(s) for this pull request + description: Link(s) to the issues this pull request is addressing. + validations: + required: true + + - type: textarea + id: change-summary + attributes: + label: Summary of the changes being made by this pull request + description: An overview of the changes you've made and why they are necessary. + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context or screenshots related to the changes here. + validations: + required: false diff --git a/.github/workflows/build-and-publish-images.yaml b/.github/workflows/build-and-publish-images.yaml index da471b83..e2a57e15 100644 --- a/.github/workflows/build-and-publish-images.yaml +++ b/.github/workflows/build-and-publish-images.yaml @@ -24,6 +24,8 @@ jobs: - image: training-portal - image: secrets-manager - image: tunnel-manager + - image: image-cache + - image: assets-server steps: - name: Check out the repository @@ -312,6 +314,9 @@ jobs: - name: Build educates client program shell: bash run: | + rm -rf client-programs/pkg/renderer/files + mkdir client-programs/pkg/renderer/files + cp -rp workshop-images/base-environment/opt/eduk8s/etc/themes client-programs/pkg/renderer/files/ cd client-programs REPOSITORY_TAG=${GITHUB_REF##*/} go build -o educates-linux-amd64 -ldflags "-X 'main.projectVersion=$REPOSITORY_TAG'" cmd/educates/main.go @@ -337,6 +342,9 @@ jobs: - name: Build educates client program shell: bash run: | + rm -rf client-programs/pkg/renderer/files + mkdir client-programs/pkg/renderer/files + cp -rp workshop-images/base-environment/opt/eduk8s/etc/themes client-programs/pkg/renderer/files/ cd client-programs REPOSITORY_TAG=${GITHUB_REF##*/} # DO NOT USE GOOS/GOARCH for native build as it appears to produce a @@ -367,6 +375,9 @@ jobs: - name: Build educates client program shell: bash run: | + rm -rf client-programs/pkg/renderer/files + mkdir client-programs/pkg/renderer/files + cp -rp workshop-images/base-environment/opt/eduk8s/etc/themes client-programs/pkg/renderer/files/ cd client-programs REPOSITORY_TAG=${GITHUB_REF##*/} GOOS=darwin GOARCH=arm64 go build -o educates-darwin-arm64 -ldflags "-X 'main.projectVersion=$REPOSITORY_TAG'" cmd/educates/main.go @@ -464,6 +475,16 @@ jobs: run: | echo "REPOSITORY_TAG=${GITHUB_REF##*/}" >>${GITHUB_ENV} + - name: Generate file checksums for CLI binaries + shell: bash + run: | + sha256sum educates-linux-amd64 >> checksums.txt + sha256sum educates-darwin-amd64 >> checksums.txt + sha256sum educates-darwin-arm64 >> checksums.txt + echo "```" >> release-notes.md + cat checksums.txt >> release-notes.md + echo "```" >> release-notes.md + - name: Create release id: create_release uses: actions/create-release@v1 @@ -474,6 +495,17 @@ jobs: release_name: "educates:${{env.REPOSITORY_TAG}}" draft: false prerelease: false + body_path: release-notes.md + + - name: Upload checksums.txt + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + with: + upload_url: ${{steps.create_release.outputs.upload_url}} + asset_path: checksums.txt + asset_name: checksums.txt + asset_content_type: text/plain - name: Upload educates-cluster-essentials.yaml uses: actions/upload-release-asset@v1 diff --git a/.gitignore b/.gitignore index e49775a2..817ac6ae 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__ /carvel-packages/training-platform/bundle/.imgpkg/images.yml /carvel-packages/training-platform/bundle/kbld-images.yaml /client-programs/educates +/client-programs/pkg/renderer/files /session-manager/venv /secrets-manager/venv /tunnel-manager/venv @@ -18,4 +19,4 @@ __pycache__ /workshop-images/base-environment/opt/helper/out /workshop-images/base-environment/opt/renderer/build /workshop-images/base-environment/opt/renderer/node_modules -/testing +/developer-testing diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..410e2351 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,127 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in this project and our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at oss-coc@vmware.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..bce083a0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing to Educates + +We welcome contributions from the community and first want to thank you for taking the time to contribute! + +Please be aware that the design of the Educates platform allows for accomplishing many tasks without directly altering the core platform. Therefore, the guiding principle for this project is "discuss before code." If your intention is to contribute a pull request that goes beyond a simple typo correction or obvious bug fix, it's recommended to initiate a discussion by first raising an issue. This enables us to assess proposed modifications and ascertain whether they necessitate adjustments to the core platform or if there's a more effective approach to achieve the desired outcome without making modifications. + +Even for seemingly minor alterations, we acknowledge that it might be more efficient for us to implement the changes. Feel free to articulate your objectives through an issue without any hesitation, and there's no pressure to create a pull request for the alteration. Our primary goal is to see you leverage Educates without unnecessary delays caused by configuration concerns. + +## Ways to Contribute + +We welcome many different types of contributions and not all of them need a pull request. Contributions may include: + +* New features and proposals +* Documentation +* Bug fixes +* Issue Triage +* Answering questions and giving feedback +* Helping to onboard new contributors +* Other related activities + +## Code of Conduct + +Please familiarize yourself with the [Code of Conduct](CODE_OF_CONDUCT.md) before contributing. + +## Contributor License Agreement + +Before you start working with Educates, please read our Contributor License Agreement [CLA](https://cla.vmware.com/cla/1/preview). If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue and walk you through the process when you open your first pull request. For any questions about the CLA process, please refer to our [FAQ]([https://cla.vmware.com/faq](https://cla.vmware.com/faq)). + +## Getting started + +If your primary objective is to use Educates, check out the [Educates user documentation](https://docs.educates.dev/). + +If you want to contribute changes to Educates, check out the [Educates developer documentation](developer-docs/README.md). + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +* Make a fork of the repository within your GitHub account +* Create a topic branch in your fork from where you want to base your work +* Make commits of logical units +* Make sure your commit messages are with the proper format, quality and descriptiveness (see below) +* Push your changes to the topic branch in your fork +* Create a pull request containing that commit + +We follow the GitHub workflow and you can find more details on the [GitHub flow documentation](https://docs.github.com/en/get-started/quickstart/github-flow). + +Before submitting your pull request, we advise you to use the following: + +### Pull Request Checklist + +1. Check if your code changes will pass any code linting checks and unit tests which may be used by the project. +2. Ensure your commit messages are descriptive. We follow the conventions on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/). Be sure to include any related GitHub issue references in the commit message. See [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) for referencing issues and commits. +3. Check the commits and commits messages and ensure they are free from typos. + +## Reporting Bugs and Creating Issues + +For specifics on what to include in your report, please follow the guidelines in the issue and pull request templates when available. + +## Asking for Help + +The best way to reach us with a question when contributing is to ask on: + +* The original GitHub issue diff --git a/workshop-images/base-environment/LICENSE b/LICENSE similarity index 89% rename from workshop-images/base-environment/LICENSE rename to LICENSE index d6456956..1a9893b4 100644 --- a/workshop-images/base-environment/LICENSE +++ b/LICENSE @@ -1,5 +1,4 @@ - - Apache License + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -175,28 +174,3 @@ of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 00000000..0ae6ceaf --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,6 @@ +# Current Educates Maintainers + +| Maintainer | GitHub ID | Affiliation | +|------------------|-------------------------------------------------------|------------------------------------------| +| Graham Dumpleton | [GrahamDumpleton](https://github.com/GrahamDumpleton) | [VMware](https://www.github.com/vmware/) | +| Jorge Morales | [jorgemoralespou](https://github.com/jorgemoralespou) | [VMware](https://www.github.com/vmware/) | diff --git a/Makefile b/Makefile index e77d7ffa..4fe15200 100644 --- a/Makefile +++ b/Makefile @@ -18,20 +18,24 @@ all: push-all-images deploy-cluster-essentials deploy-training-platform deploy-w build-all-images: build-session-manager build-training-portal \ build-base-environment build-jdk8-environment build-jdk11-environment \ build-jdk17-environment build-conda-environment build-docker-registry \ - build-pause-container build-secrets-manager build-tunnel-manager + build-pause-container build-secrets-manager build-tunnel-manager \ + build-image-cache build-assets-server push-all-images: push-session-manager push-training-portal \ push-base-environment push-jdk8-environment push-jdk11-environment \ push-jdk17-environment push-conda-environment push-docker-registry \ - push-pause-container push-secrets-manager push-tunnel-manager + push-pause-container push-secrets-manager push-tunnel-manager \ + push-image-cache push-assets-server build-core-images: build-session-manager build-training-portal \ build-base-environment build-docker-registry build-pause-container \ - build-secrets-manager build-tunnel-manager + build-secrets-manager build-tunnel-manager build-image-cache \ + build-assets-server push-core-images: push-session-manager push-training-portal \ push-base-environment push-docker-registry push-pause-container \ - push-secrets-manager push-tunnel-manager + push-secrets-manager push-tunnel-manager push-image-cache \ + push-assets-server build-session-manager: docker build --platform $(DOCKER_PLATFORM) -t $(IMAGE_REPOSITORY)/educates-session-manager:$(PACKAGE_VERSION) session-manager @@ -105,9 +109,21 @@ build-tunnel-manager: push-tunnel-manager: build-tunnel-manager docker push $(IMAGE_REPOSITORY)/educates-tunnel-manager:$(PACKAGE_VERSION) +build-image-cache: + docker build --platform $(DOCKER_PLATFORM) -t $(IMAGE_REPOSITORY)/educates-image-cache:$(PACKAGE_VERSION) image-cache + +push-image-cache: build-image-cache + docker push $(IMAGE_REPOSITORY)/educates-image-cache:$(PACKAGE_VERSION) + +build-assets-server: + docker build --platform $(DOCKER_PLATFORM) -t $(IMAGE_REPOSITORY)/educates-assets-server:$(PACKAGE_VERSION) assets-server + +push-assets-server: build-assets-server + docker push $(IMAGE_REPOSITORY)/educates-assets-server:$(PACKAGE_VERSION) + verify-cluster-essentials-config: -ifneq ("$(wildcard testing/educates-cluster-essentials-values.yaml)","") - @ytt --file carvel-packages/cluster-essentials/bundle/config --data-values-file testing/educates-cluster-essentials-values.yaml +ifneq ("$(wildcard developer-testing/educates-cluster-essentials-values.yaml)","") + @ytt --file carvel-packages/cluster-essentials/bundle/config --data-values-file developer-testing/educates-cluster-essentials-values.yaml else @ytt --file carvel-packages/cluster-essentials/bundle/config endif @@ -115,13 +131,13 @@ endif push-cluster-essentials-bundle: ytt -f carvel-packages/cluster-essentials/bundle/config | kbld -f - --imgpkg-lock-output carvel-packages/cluster-essentials/bundle/.imgpkg/images.yml imgpkg push -b $(IMAGE_REPOSITORY)/educates-cluster-essentials:$(RELEASE_VERSION) -f carvel-packages/cluster-essentials/bundle - mkdir -p testing - ytt -f carvel-packages/cluster-essentials/bundle --data-values-schema-inspect -o openapi-v3 > testing/educates-cluster-essentials-schema-openapi.yaml - ytt -f carvel-packages/cluster-essentials/config/package.yaml -f carvel-packages/cluster-essentials/config/schema.yaml -v imageRegistry.host=$(IMAGE_REPOSITORY) -v version=$(RELEASE_VERSION) -v releasedAt=`date -u +"%Y-%m-%dT%H:%M:%SZ"` --data-value-file openapi=testing/educates-cluster-essentials-schema-openapi.yaml > testing/educates-cluster-essentials.yaml + mkdir -p developer-testing + ytt -f carvel-packages/cluster-essentials/bundle --data-values-schema-inspect -o openapi-v3 > developer-testing/educates-cluster-essentials-schema-openapi.yaml + ytt -f carvel-packages/cluster-essentials/config/package.yaml -f carvel-packages/cluster-essentials/config/schema.yaml -v imageRegistry.host=$(IMAGE_REPOSITORY) -v version=$(RELEASE_VERSION) -v releasedAt=`date -u +"%Y-%m-%dT%H:%M:%SZ"` --data-value-file openapi=developer-testing/educates-cluster-essentials-schema-openapi.yaml > developer-testing/educates-cluster-essentials.yaml deploy-cluster-essentials: -ifneq ("$(wildcard testing/educates-cluster-essentials-values.yaml)","") - ytt --file carvel-packages/cluster-essentials/bundle/config --data-values-file testing/educates-cluster-essentials-values.yaml | kapp deploy -a educates-cluster-essentials -f - -y +ifneq ("$(wildcard developer-testing/educates-cluster-essentials-values.yaml)","") + ytt --file carvel-packages/cluster-essentials/bundle/config --data-values-file developer-testing/educates-cluster-essentials-values.yaml | kapp deploy -a educates-cluster-essentials -f - -y else ytt --file carvel-packages/cluster-essentials/bundle/config | kapp deploy -a educates-cluster-essentials -f - -y endif @@ -132,9 +148,9 @@ delete-cluster-essentials: deploy-cluster-essentials-bundle: kubectl get ns/educates-package || kubectl create ns educates-package kubectl apply --namespace educates-package -f package-repository/packages/cluster-essentials.educates.dev/metadata.yaml - kubectl apply --namespace educates-package -f testing/educates-cluster-essentials.yaml -ifneq ("$(wildcard testing/educates-cluster-essentials-values.yaml)","") - kctrl package install --namespace educates-package --package-install educates-cluster-essentials --package cluster-essentials.educates.dev --version $(RELEASE_VERSION) --values-file testing/educates-cluster-essentials-values.yaml + kubectl apply --namespace educates-package -f developer-testing/educates-cluster-essentials.yaml +ifneq ("$(wildcard developer-testing/educates-cluster-essentials-values.yaml)","") + kctrl package install --namespace educates-package --package-install educates-cluster-essentials --package cluster-essentials.educates.dev --version $(RELEASE_VERSION) --values-file developer-testing/educates-cluster-essentials-values.yaml else kctrl package install --namespace educates-package --package-install educates-cluster-essentials --package cluster-essentials.educates.dev --version $(RELEASE_VERSION) endif @@ -143,8 +159,8 @@ delete-cluster-essentials-bundle: kctrl package installed delete --namespace educates-package --package-install educates-cluster-essentials -y verify-training-platform-config: -ifneq ("$(wildcard testing/educates-training-platform-values.yaml)","") - @ytt --file carvel-packages/training-platform/bundle/config --data-values-file testing/educates-training-platform-values.yaml +ifneq ("$(wildcard developer-testing/educates-training-platform-values.yaml)","") + @ytt --file carvel-packages/training-platform/bundle/config --data-values-file developer-testing/educates-training-platform-values.yaml else @ytt --file carvel-packages/training-platform/bundle/config endif @@ -153,13 +169,13 @@ push-training-platform-bundle: ytt -f carvel-packages/training-platform/config/images.yaml -f carvel-packages/training-platform/config/schema.yaml -v imageRegistry.host=$(IMAGE_REPOSITORY) -v version=$(PACKAGE_VERSION) > carvel-packages/training-platform/bundle/kbld-images.yaml cat carvel-packages/training-platform/bundle/kbld-images.yaml | kbld -f - --imgpkg-lock-output carvel-packages/training-platform/bundle/.imgpkg/images.yml imgpkg push -b $(IMAGE_REPOSITORY)/educates-training-platform:$(RELEASE_VERSION) -f carvel-packages/training-platform/bundle - mkdir -p testing - ytt -f carvel-packages/training-platform/bundle --data-values-schema-inspect -o openapi-v3 > testing/educates-training-platform-schema-openapi.yaml - ytt -f carvel-packages/training-platform/config/package.yaml -f carvel-packages/training-platform/config/schema.yaml -v imageRegistry.host=$(IMAGE_REPOSITORY) -v version=$(RELEASE_VERSION) -v releasedAt=`date -u +"%Y-%m-%dT%H:%M:%SZ"` --data-value-file openapi=testing/educates-training-platform-schema-openapi.yaml > testing/educates-training-platform.yaml + mkdir -p developer-testing + ytt -f carvel-packages/training-platform/bundle --data-values-schema-inspect -o openapi-v3 > developer-testing/educates-training-platform-schema-openapi.yaml + ytt -f carvel-packages/training-platform/config/package.yaml -f carvel-packages/training-platform/config/schema.yaml -v imageRegistry.host=$(IMAGE_REPOSITORY) -v version=$(RELEASE_VERSION) -v releasedAt=`date -u +"%Y-%m-%dT%H:%M:%SZ"` --data-value-file openapi=developer-testing/educates-training-platform-schema-openapi.yaml > developer-testing/educates-training-platform.yaml deploy-training-platform: -ifneq ("$(wildcard testing/educates-training-platform-values.yaml)","") - ytt --file carvel-packages/training-platform/bundle/config --data-values-file testing/educates-training-platform-values.yaml | kapp deploy -a educates-training-platform -f - -y +ifneq ("$(wildcard developer-testing/educates-training-platform-values.yaml)","") + ytt --file carvel-packages/training-platform/bundle/config --data-values-file developer-testing/educates-training-platform-values.yaml | kapp deploy -a educates-training-platform -f - -y else ytt --file carvel-packages/training-platform/bundle/config | kapp deploy -a educates-training-platform -f - -y endif @@ -174,9 +190,9 @@ delete-training-platform: delete-workshop deploy-training-platform-bundle: kubectl get ns/educates-package || kubectl create ns educates-package kubectl apply --namespace educates-package -f package-repository/packages/training-platform.educates.dev/metadata.yaml - kubectl apply --namespace educates-package -f testing/educates-training-platform.yaml -ifneq ("$(wildcard testing/educates-training-platform-values.yaml)","") - kctrl package install --namespace educates-package --package-install educates-training-platform --package training-platform.educates.dev --version $(RELEASE_VERSION) --values-file testing/educates-training-platform-values.yaml + kubectl apply --namespace educates-package -f developer-testing/educates-training-platform.yaml +ifneq ("$(wildcard developer-testing/educates-training-platform-values.yaml)","") + kctrl package install --namespace educates-package --package-install educates-training-platform --package training-platform.educates.dev --version $(RELEASE_VERSION) --values-file developer-testing/educates-training-platform-values.yaml else kctrl package install --namespace educates-package --package-install educates-training-platform --package training-platform.educates.dev --version $(RELEASE_VERSION) endif @@ -185,6 +201,9 @@ delete-training-platform-bundle: kctrl package installed delete --namespace educates-package --package-install educates-training-platform -y client-programs-educates: + rm -rf client-programs/pkg/renderer/files + mkdir client-programs/pkg/renderer/files + cp -rp workshop-images/base-environment/opt/eduk8s/etc/themes client-programs/pkg/renderer/files/ (cd client-programs; go build -o educates cmd/educates/main.go) client-programs: client-programs-educates @@ -215,6 +234,7 @@ prune-builds: rm -rf workshop-images/base-environment/opt/renderer/node_modules rm -rf training-portal/venv rm -f client-programs/educates + rm -rf client-programs/pkg/renderer/files prune-registry: docker exec educates-registry registry garbage-collect /etc/docker/registry/config.yml --delete-untagged=true diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..a31a77d9 --- /dev/null +++ b/NOTICE @@ -0,0 +1,13 @@ +Copyright 2020-2023 The Educates Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index ecacf9e9..7e15bd32 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,14 @@ Educates Training Platform ========================== The Educates project provides a system for hosting interactive workshop -environments in Kubernetes. It can be used for self paced or supervised -workshops. It can also be useful where you need to package up demos of -applications hosted in Kubernetes for users or potential customers. +environments in Kubernetes, or on top of a local container runtime. It can be +used for self paced or supervised workshops. It can also be useful where you +need to package up demos of applications hosted in Kubernetes or a local +container runtime. For detailed instructions on how to deploy and make use of Educates see the -[Educates documentation](https://github.com/vmware-tanzu-labs/educates-docs). +[Educates user documentation](https://docs.educates.dev/). + +The Educates project exists because of the contributions of our +[maintainers](MAINTAINERS.md). If you would like to contribute to Educates, +check out our [contribution guidelines](CONTRIBUTING.md). diff --git a/assets-server/Dockerfile b/assets-server/Dockerfile new file mode 100644 index 00000000..9ee35518 --- /dev/null +++ b/assets-server/Dockerfile @@ -0,0 +1,28 @@ +FROM golang:1.19-buster as builder-image + +WORKDIR /app + +COPY . /app/ + +RUN go mod download && \ + go build -o assets-server main.go + +FROM fedora:36 + +RUN useradd -u 1001 -g 0 -M -d /opt/app-root/src default && \ + mkdir -p /opt/app-root/src && \ + chown -R 1001:0 /opt/app-root + +WORKDIR /opt/app-root + +COPY --from=builder-image /app/assets-server /opt/app-root/bin/ + +USER 1001 + +EXPOSE 8080 + +VOLUME ["/opt/app-root/data"] + +ENTRYPOINT ["/opt/app-root/bin/assets-server"] + +CMD ["--dir", "/opt/app-root/data", "--host", "0.0.0.0"] diff --git a/assets-server/go.mod b/assets-server/go.mod new file mode 100644 index 00000000..ccaa044f --- /dev/null +++ b/assets-server/go.mod @@ -0,0 +1,9 @@ +module github.com/vmware-tanzu-labs/educates-training-platform/assets-server + +go 1.20 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/assets-server/go.sum b/assets-server/go.sum new file mode 100644 index 00000000..f3366a91 --- /dev/null +++ b/assets-server/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/assets-server/main.go b/assets-server/main.go new file mode 100644 index 00000000..32c1cf93 --- /dev/null +++ b/assets-server/main.go @@ -0,0 +1,269 @@ +/* + * This is a Golang application that serves static files from a specified + * directory and can also create and serve tar and zip archives of directories + * from the same directory. + * + * The application uses the cobra package for command-line argument handling. It + * allows the user to specify the directory path from which static files are + * served, the port the server listens on, and the host interface the listener + * socket is bound to. + * + * The server can handle the following types of requests: + * - Requests for regular static files (e.g., http://localhost:8080/file.txt) + * - Requests for tar archives of directories (e.g., http://localhost:8080/subdir/.tar) + * - Requests for tar.gz or .tgz archives of directories (e.g., http://localhost:8080/subdir/.tar.gz) + * - Requests for zip archives of directories (e.g., http://localhost:8080/subdir/.zip) + */ + +package main + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +func createTarArchive(dirPath string, writer io.Writer, compress bool) error { + var tarWriter *tar.Writer + if compress { + gzipWriter := gzip.NewWriter(writer) + defer gzipWriter.Close() + tarWriter = tar.NewWriter(gzipWriter) + } else { + tarWriter = tar.NewWriter(writer) + } + defer tarWriter.Close() + + return filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(dirPath, path) + if err != nil { + return err + } + + // Create a new tar header + header, err := tar.FileInfoHeader(info, "") + if err != nil { + return err + } + header.Name = filepath.ToSlash(relPath) + + // Write the header to the tar archive + if err := tarWriter.WriteHeader(header); err != nil { + return err + } + + // If the file is not a directory, write its content to the tar archive + if !info.IsDir() { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(tarWriter, file) + if err != nil { + return err + } + } + + return nil + }) +} + +func createZipArchive(dirPath string, writer io.Writer) error { + zipWriter := zip.NewWriter(writer) + defer zipWriter.Close() + + return filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(dirPath, path) + if err != nil { + return err + } + + // Skip adding directory entries as vendir will fail handling it + if info.IsDir() { + return nil + } + + // Create a new zip header + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Name = filepath.ToSlash(relPath) + + // Write the header to the zip archive + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + // If the file is not a directory, write its content to the zip archive + if !info.IsDir() { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(writer, file) + if err != nil { + return err + } + } + + return nil + }) +} + +func main() { + var rootCmd = &cobra.Command{ + Use: "static-server", + Short: "Serve static files from a directory", + Run: startServer, + } + + var dataDir string + var port string + var host string + + rootCmd.Flags().StringVarP(&dataDir, "dir", "d", "data", "Directory path containing static files") + rootCmd.Flags().StringVarP(&port, "port", "p", "8080", "Port number to listen on") + rootCmd.Flags().StringVarP(&host, "host", "H", "localhost", "Host interface to bind the listener socket") + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func startServer(cmd *cobra.Command, args []string) { + dataDir, _ := cmd.Flags().GetString("dir") + port, _ := cmd.Flags().GetString("port") + host, _ := cmd.Flags().GetString("host") + + // Check if the data directory exists + _, err := os.Stat(dataDir) + if err != nil { + if os.IsNotExist(err) { + fmt.Println("Directory", dataDir, "does not exist. Please create the directory and put your static files in it.") + return + } + fmt.Println("Error:", err) + return + } + + // Middleware for logging HTTP requests + loggingMiddleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("Incoming request: %s %s", r.Method, r.URL.Path) + next.ServeHTTP(w, r) + }) + } + + // Create a file server handler to serve static files from the data directory + fileServer := http.FileServer(http.Dir(dataDir)) + + // Handle requests for tar, tar.gz, or zip archives of directories + http.Handle("/", loggingMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestedPath := r.URL.Path + + // Check if the requested path ends with ".tar", ".tar.gz" or ".tgz" + if strings.HasSuffix(requestedPath, "/.tar") { + // Remove the ".tar" suffix from the path + requestedPath = strings.TrimSuffix(requestedPath, ".tar") + + // Check if the path maps to a directory + fileInfo, err := os.Stat(filepath.Join(dataDir, requestedPath)) + if err != nil || !fileInfo.IsDir() { + // Serve static files as the path does not map to a directory + fileServer.ServeHTTP(w, r) + return + } + + // Serve the tar archive for the requested directory + w.Header().Set("Content-Type", "application/x-tar") + w.Header().Set("Content-Disposition", "attachment; filename=\"assets.tar\"") + + err = createTarArchive(filepath.Join(dataDir, requestedPath), w, false) + if err != nil { + http.Error(w, "Error creating tar archive", http.StatusInternalServerError) + return + } + return + } else if strings.HasSuffix(requestedPath, "/.tar.gz") || strings.HasSuffix(requestedPath, "/.tgz") { + // Remove the ".tar.gz" or ".tgz" suffix from the path + requestedPath = strings.TrimSuffix(requestedPath, ".tar.gz") + requestedPath = strings.TrimSuffix(requestedPath, ".tgz") + + // Check if the path maps to a directory + fileInfo, err := os.Stat(filepath.Join(dataDir, requestedPath)) + if err != nil || !fileInfo.IsDir() { + // Serve static files as the path does not map to a directory + fileServer.ServeHTTP(w, r) + return + } + + // Serve the tar.gz archive for the requested directory + w.Header().Set("Content-Type", "application/gzip") + w.Header().Set("Content-Disposition", "attachment; filename=\"assets.tar.gz\"") + + err = createTarArchive(filepath.Join(dataDir, requestedPath), w, true) + if err != nil { + http.Error(w, "Error creating tar.gz archive", http.StatusInternalServerError) + return + } + return + } else if strings.HasSuffix(requestedPath, "/.zip") { + // Remove the ".zip" suffix from the path + requestedPath = strings.TrimSuffix(requestedPath, ".zip") + + // Check if the path maps to a directory + fileInfo, err := os.Stat(filepath.Join(dataDir, requestedPath)) + if err != nil || !fileInfo.IsDir() { + // Serve static files as the path does not map to a directory + fileServer.ServeHTTP(w, r) + return + } + + // Serve the zip archive for the requested directory + w.Header().Set("Content-Type", "application/zip") + w.Header().Set("Content-Disposition", "attachment; filename=\"assets.zip\"") + + err = createZipArchive(filepath.Join(dataDir, requestedPath), w) + if err != nil { + http.Error(w, "Error creating zip archive", http.StatusInternalServerError) + return + } + return + } + + // Serve static files + fileServer.ServeHTTP(w, r) + }))) + + // Start the server on the specified host and port + addr := host + ":" + port + fmt.Println("Server is running on http://" + addr) + err = http.ListenAndServe(addr, nil) + if err != nil { + fmt.Println("Error:", err) + } +} diff --git a/carvel-packages/cluster-essentials/config/package.yaml b/carvel-packages/cluster-essentials/config/package.yaml index 9fc63067..30922305 100644 --- a/carvel-packages/cluster-essentials/config/package.yaml +++ b/carvel-packages/cluster-essentials/config/package.yaml @@ -3,8 +3,8 @@ #@ def bundle_reference(): #@ registry = data.values.imageRegistry.host -#@ if not registry or registry == "localhost:5001": -#@ registry = "registry.default.svc.cluster.local:5001" +#@ if not registry or registry == "localhost": +#@ registry = "registry.default.svc.cluster.local" #@ end #@ if data.values.imageRegistry.namespace: #@ registry = "{}/{}".format(registry, data.values.imageRegistry.namespace) diff --git a/carvel-packages/cluster-essentials/config/schema.yaml b/carvel-packages/cluster-essentials/config/schema.yaml index a07ca0bd..c55e3cbc 100644 --- a/carvel-packages/cluster-essentials/config/schema.yaml +++ b/carvel-packages/cluster-essentials/config/schema.yaml @@ -4,7 +4,7 @@ version: latest imageRegistry: - host: "localhost:5001" + host: "localhost" namespace: "" releasedAt: "" diff --git a/carvel-packages/training-platform/bundle/config/00-package.star b/carvel-packages/training-platform/bundle/config/00-package.star index 1c819397..ba5d602e 100644 --- a/carvel-packages/training-platform/bundle/config/00-package.star +++ b/carvel-packages/training-platform/bundle/config/00-package.star @@ -22,7 +22,7 @@ end def image_reference(name): registry = data.values.imageRegistry.host if not registry: - registry = "registry.default.svc.cluster.local:5001" + registry = "registry.default.svc.cluster.local" end if data.values.imageRegistry.namespace: registry = "{}/{}".format(registry, data.values.imageRegistry.namespace) diff --git a/carvel-packages/training-platform/bundle/config/00-schema.yaml b/carvel-packages/training-platform/bundle/config/00-schema.yaml index e5c59844..95510c27 100644 --- a/carvel-packages/training-platform/bundle/config/00-schema.yaml +++ b/carvel-packages/training-platform/bundle/config/00-schema.yaml @@ -121,6 +121,17 @@ clusterIngress: enabled: false +#! Settings for overriding options for portal and workshop session cookies. + +sessionCookies: + + #! Session cookie domain. DNS parent domain used for training portal and + #! workshop session cookies. May need to be set to a parent domain of the + #! ingress domain if cross domain cookie sharing is necessary due to + #! embedding. + + domain: "" + #! Configuration for persistent volumes. The default storage class specified #! by the cluster will be used if not defined. Storage group may need to be #! set where a cluster has pod security policies enabled, usually setting it @@ -258,10 +269,15 @@ websiteStyling: script: "" style: "" + defaultTheme: "" + themeDataRefs: - name: "" namespace: "" + frameAncestors: + - "" + #! Pre-pull selected Educates images to nodes in the cluster. Should be empty #! list if no images should be prepulled. This is done to reduce start up times #! for workhop sessions the first time on each node in the cluster. diff --git a/carvel-packages/training-platform/bundle/config/00-values.yaml b/carvel-packages/training-platform/bundle/config/00-values.yaml index 50d8e0ee..3977109e 100644 --- a/carvel-packages/training-platform/bundle/config/00-values.yaml +++ b/carvel-packages/training-platform/bundle/config/00-values.yaml @@ -16,8 +16,6 @@ imageVersions: image: "rancher/k3s:v1.25.3-k3s1" - name: loftsh-vcluster image: "loftsh/vcluster:0.13.0" -- name: nginx-server - image: "bitnami/nginx:1.22.1" - name: contour-bundle #! contour.community.tanzu.vmware.com.1.22.0 image: "projects.registry.vmware.com/tce/contour@sha256:b68ad8ec3012db7d2a2e84f8544685012e2dca09d28d54dce8735fb60f0d05bf" diff --git a/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-trainingportal.yaml b/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-trainingportal.yaml index a7f2a7dd..d93ff134 100644 --- a/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-trainingportal.yaml +++ b/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-trainingportal.yaml @@ -90,6 +90,9 @@ spec: overdue: type: string pattern: '^\d+(s|m|h)$' + refresh: + type: string + pattern: '^\d+(s|m|h)$' registry: type: object required: @@ -136,6 +139,11 @@ spec: type: string namespace: type: string + cookies: + type: object + properties: + domain: + type: string registration: type: object properties: @@ -244,6 +252,9 @@ spec: overdue: type: string pattern: '^\d+(s|m|h)$' + refresh: + type: string + pattern: '^\d+(s|m|h)$' registry: type: object required: diff --git a/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshop.yaml b/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshop.yaml index 11e21934..156a091b 100644 --- a/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshop.yaml +++ b/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshop.yaml @@ -39,6 +39,8 @@ spec: type: array items: type: string + version: + type: string difficulty: type: string pattern: '^(beginner|intermediate|advanced|extreme)$' @@ -60,6 +62,74 @@ spec: type: string files: type: string + publish: + type: object + properties: + image: + type: string + files: + type: array + items: + type: object + oneOf: + - required: + - git + - required: + - hg + - required: + - http + - required: + - image + - required: + - imgpkgBundle + - required: + - githubRelease + - required: + - helmChart + - required: + - directory + properties: + path: + type: string + default: "." + git: + type: object + x-kubernetes-preserve-unknown-fields: true + hg: + type: object + x-kubernetes-preserve-unknown-fields: true + http: + type: object + x-kubernetes-preserve-unknown-fields: true + image: + type: object + x-kubernetes-preserve-unknown-fields: true + imgpkgBundle: + type: object + x-kubernetes-preserve-unknown-fields: true + githubRelease: + type: object + x-kubernetes-preserve-unknown-fields: true + helmChart: + type: object + x-kubernetes-preserve-unknown-fields: true + directory: + type: object + x-kubernetes-preserve-unknown-fields: true + includePaths: + type: array + items: + type: string + excludePaths: + type: array + items: + type: string + legalPaths: + type: array + items: + type: string + newRootPath: + type: string workshop: type: object properties: @@ -293,7 +363,6 @@ spec: type: string memory: type: string - default: 128Mi files: type: array items: @@ -383,6 +452,62 @@ spec: type: string newRootPath: type: string + images: + type: object + properties: + ingress: + type: object + properties: + enabled: + type: boolean + default: false + storage: + type: string + memory: + type: string + registries: + type: array + items: + type: object + required: + - urls + properties: + urls: + type: array + items: + type: string + onDemand: + type: boolean + pollInterval: + type: string + tlsVerify: + type: boolean + maxRetries: + type: integer + retryDelay: + type: string + onlySigned: + type: boolean + content: + type: array + items: + type: object + properties: + prefix: + type: string + destination: + type: string + stripPrefix: + type: boolean + tags: + type: object + required: + - regex + properties: + regex: + type: string + semver: + type: boolean session: type: object properties: @@ -628,6 +753,16 @@ spec: name: type: string x-kubernetes-preserve-unknown-fields: true + initContainers: + type: array + items: + type: object + required: + - name + properties: + name: + type: string + x-kubernetes-preserve-unknown-fields: true applications: type: object properties: @@ -636,14 +771,54 @@ spec: properties: enabled: type: boolean + #! The renderer property is now obsolete and should not be used. renderer: type: string enum: - local - remote - static + - proxy url: type: string + proxy: + type: object + required: + - host + properties: + protocol: + type: string + host: + type: string + port: + type: integer + headers: + type: array + items: + type: object + required: + - name + properties: + name: + type: string + value: + type: string + changeOrigin: + type: boolean + pathRewrite: + type: array + items: + type: object + required: + - pattern + - replacement + properties: + pattern: + type: string + replacement: + type: string + path: + type: string layout: type: string terminal: @@ -848,7 +1023,6 @@ spec: type: object required: - name - - url properties: name: type: string @@ -860,6 +1034,7 @@ spec: type: object required: - name + - host properties: name: type: string @@ -918,6 +1093,18 @@ spec: items: type: object x-kubernetes-preserve-unknown-fields: true + images: + type: array + items: + type: object + required: + - name + - image + properties: + name: + type: string + image: + type: string status: type: object x-kubernetes-preserve-unknown-fields: true diff --git a/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshopenvironment.yaml b/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshopenvironment.yaml index 7af3884f..50ccb26b 100644 --- a/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshopenvironment.yaml +++ b/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshopenvironment.yaml @@ -135,6 +135,11 @@ spec: properties: name: type: string + cookies: + type: object + properties: + domain: + type: string status: type: object x-kubernetes-preserve-unknown-fields: true diff --git a/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshopsession.yaml b/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshopsession.yaml index 3b98a99c..b55fc2d2 100644 --- a/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshopsession.yaml +++ b/carvel-packages/training-platform/bundle/config/11-session-manager/01-crds-workshopsession.yaml @@ -28,6 +28,23 @@ spec: required: - environment properties: + workshop: + type: object + required: + - name + properties: + name: + type: string + portal: + type: object + required: + - name + - url + properties: + name: + type: string + url: + type: string environment: type: object required: @@ -46,6 +63,11 @@ spec: type: string password: type: string + config: + type: object + properties: + password: + type: string ingress: type: object properties: diff --git a/carvel-packages/training-platform/config/images.yaml b/carvel-packages/training-platform/config/images.yaml index 19644721..836643bc 100644 --- a/carvel-packages/training-platform/config/images.yaml +++ b/carvel-packages/training-platform/config/images.yaml @@ -2,8 +2,8 @@ #@ def image_reference(name): #@ registry = data.values.imageRegistry.host -#@ if not registry or registry == "localhost:5001": -#@ registry = "registry.default.svc.cluster.local:5001" +#@ if not registry or registry == "localhost": +#@ registry = "registry.default.svc.cluster.local" #@ end #@ if data.values.imageRegistry.namespace: #@ registry = "{}/{}".format(registry, data.values.imageRegistry.namespace) @@ -35,6 +35,10 @@ imageVersions: image: #@ image_reference("secrets-manager") - name: tunnel-manager image: #@ image_reference("tunnel-manager") +- name: image-cache + image: #@ image_reference("image-cache") +- name: assets-server + image: #@ image_reference("assets-server") - name: debian-base-image image: "debian:sid-20230502-slim" - name: docker-in-docker @@ -49,8 +53,6 @@ imageVersions: image: "rancher/k3s:v1.25.3-k3s1" - name: loftsh-vcluster image: "loftsh/vcluster:0.13.0" -- name: nginx-server - image: "bitnami/nginx:1.22.1" - name: contour-bundle #! contour.community.tanzu.vmware.com.1.22.0 image: "projects.registry.vmware.com/tce/contour@sha256:b68ad8ec3012db7d2a2e84f8544685012e2dca09d28d54dce8735fb60f0d05bf" diff --git a/carvel-packages/training-platform/config/package.yaml b/carvel-packages/training-platform/config/package.yaml index b7e26700..aad4a94a 100644 --- a/carvel-packages/training-platform/config/package.yaml +++ b/carvel-packages/training-platform/config/package.yaml @@ -3,8 +3,8 @@ #@ def bundle_reference(): #@ registry = data.values.imageRegistry.host -#@ if not registry or registry == "localhost:5001": -#@ registry = "registry.default.svc.cluster.local:5001" +#@ if not registry or registry == "localhost": +#@ registry = "registry.default.svc.cluster.local" #@ end #@ if data.values.imageRegistry.namespace: #@ registry = "{}/{}".format(registry, data.values.imageRegistry.namespace) diff --git a/carvel-packages/training-platform/config/schema.yaml b/carvel-packages/training-platform/config/schema.yaml index a07ca0bd..c55e3cbc 100644 --- a/carvel-packages/training-platform/config/schema.yaml +++ b/carvel-packages/training-platform/config/schema.yaml @@ -4,7 +4,7 @@ version: latest imageRegistry: - host: "localhost:5001" + host: "localhost" namespace: "" releasedAt: "" diff --git a/client-programs/go.mod b/client-programs/go.mod index 5aa91da9..0881730c 100644 --- a/client-programs/go.mod +++ b/client-programs/go.mod @@ -7,27 +7,32 @@ require ( github.com/compose-spec/compose-go v1.7.0 github.com/cppforlife/go-cli-ui v0.0.0-20220622150351-995494831c6c github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker v20.10.25+incompatible + github.com/docker/docker v23.0.3+incompatible github.com/docker/go-connections v0.4.0 github.com/gorilla/websocket v1.5.0 github.com/joho/godotenv v1.5.1 github.com/pkg/errors v0.9.1 - github.com/spf13/cobra v1.6.1 - github.com/vmware-tanzu/carvel-imgpkg v0.33.0 - github.com/vmware-tanzu/carvel-kapp v0.53.0 + github.com/spf13/cobra v1.7.0 + github.com/vmware-tanzu/carvel-imgpkg v0.37.2 + github.com/vmware-tanzu/carvel-kapp v0.58.0 golang.org/x/exp v0.0.0-20221111204811-129d8d6c17ab gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.25.4 - k8s.io/apimachinery v0.25.4 - k8s.io/client-go v0.25.4 - k8s.io/kubectl v0.25.4 + k8s.io/api v0.27.4 + k8s.io/apimachinery v0.27.4 + k8s.io/client-go v0.27.4 + k8s.io/kubectl v0.27.4 sigs.k8s.io/kind v0.17.0 sigs.k8s.io/yaml v1.3.0 ) require ( - cloud.google.com/go/compute v1.12.1 // indirect - cloud.google.com/go/compute/metadata v0.2.1 // indirect + github.com/vmware-tanzu/carvel-vendir v0.34.3 + github.com/vmware-tanzu/carvel-ytt v0.45.3 +) + +require ( + cloud.google.com/go/compute v1.18.0 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/Azure/azure-sdk-for-go v67.0.0+incompatible // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect @@ -41,8 +46,6 @@ require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/alessio/shellescape v1.4.1 // indirect github.com/aws/aws-sdk-go-v2 v1.16.3 // indirect @@ -59,35 +62,38 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.16.4 // indirect github.com/aws/smithy-go v1.11.2 // indirect github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect - github.com/cheggaaa/pb/v3 v3.1.0 // indirect + github.com/bmatcuk/doublestar v1.2.1 // indirect + github.com/carvel-dev/semver/v4 v4.0.1-0.20230221220520-8090ce423695 // indirect + github.com/cheggaaa/pb/v3 v3.1.2 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.13.0 // indirect - github.com/cppforlife/cobrautil v0.0.0-20221021151949-d60711905d65 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/cppforlife/cobrautil v0.0.0-20221130162803-acdfead391ef // indirect github.com/cppforlife/color v1.9.1-0.20200716202919-6706ac40b835 // indirect github.com/cppforlife/go-patch v0.2.0 // indirect - github.com/creack/pty v1.1.17 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/distribution/distribution/v3 v3.0.0-20221111170714-3b8fbf975279 // indirect - github.com/docker/cli v20.10.24+incompatible // indirect + github.com/docker/cli v23.0.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 // indirect - github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/emicklei/go-restful/v3 v3.10.1 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/ghodss/yaml v1.0.0 // indirect + github.com/fatih/color v1.14.1 // indirect github.com/go-logr/logr v1.2.3 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.6 // indirect - github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.1 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/go-containerregistry v0.12.1 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-containerregistry v0.14.0 // indirect + github.com/google/go-github v17.0.0+incompatible // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/safetext v0.0.0-20221026122733-23539d61753f // indirect + github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -97,54 +103,51 @@ require ( github.com/k14s/difflib v0.0.0-20201117154628-0c031775bf57 // indirect github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 // indirect github.com/k14s/ytt v0.36.0 // indirect - github.com/klauspost/compress v1.15.12 // indirect - github.com/kr/pretty v0.3.0 // indirect - github.com/mailru/easyjson v0.7.6 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c // indirect + github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo v1.16.4 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc2 // indirect + github.com/otiai10/copy v1.2.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/rivo/uniseg v0.4.2 // indirect - github.com/rogpeppe/go-internal v1.8.1 // indirect - github.com/russross/blackfriday v1.6.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/vbatts/tar-split v0.11.2 // indirect github.com/vito/go-interact v1.0.1 // indirect - github.com/vmware-tanzu/carvel-kapp-controller v0.42.0 // indirect - github.com/vmware-tanzu/carvel-vendir v0.32.0 // indirect + github.com/vmware-tanzu/carvel-kapp-controller v0.46.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - golang.org/x/crypto v0.2.0 // indirect - golang.org/x/mod v0.7.0 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/oauth2 v0.2.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/term v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect golang.org/x/time v0.2.0 // indirect - golang.org/x/tools v0.3.0 // indirect + golang.org/x/tools v0.9.1 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/protobuf v1.29.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/klog/v2 v2.80.1 // indirect - k8s.io/kube-openapi v0.0.0-20220803164354-a70c9af30aea // indirect - k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2 // indirect - sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + k8s.io/klog/v2 v2.90.1 // indirect + k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect + k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) diff --git a/client-programs/go.sum b/client-programs/go.sum index dc6d97b8..10e69828 100644 --- a/client-programs/go.sum +++ b/client-programs/go.sum @@ -6,10 +6,10 @@ cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxK cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -50,11 +50,6 @@ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6 github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= @@ -109,10 +104,14 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bmatcuk/doublestar v1.2.1 h1:eetYiv8DDYOZcBADY+pRvRytf3Dlz1FhnpvL2FsClBc= +github.com/bmatcuk/doublestar v1.2.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/carvel-dev/semver/v4 v4.0.1-0.20230221220520-8090ce423695 h1:naCDnpJeqQq5OHOYR6j01yIVVUk3WI5MuSHpDTy+M1A= +github.com/carvel-dev/semver/v4 v4.0.1-0.20230221220520-8090ce423695/go.mod h1:4cFTBLAr/U11ykiEEQMccu4uJ1i0GS+atJmeETHCFtI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cheggaaa/pb/v3 v3.1.0 h1:3uouEsl32RL7gTiQsuaXD4Bzbfl5tGztXGUvXbs4O04= -github.com/cheggaaa/pb/v3 v3.1.0/go.mod h1:YjrevcBqadFDaGQKRdmZxTY42pXEqda48Ea3lt0K/BE= +github.com/cheggaaa/pb/v3 v3.1.2 h1:FIxT3ZjOj9XJl0U4o2XbEhjFfZl7jCVCDOGq1ZAB7wQ= +github.com/cheggaaa/pb/v3 v3.1.2/go.mod h1:SNjnd0yKcW+kw0brSusraeDd5Bf1zBfxAzTL2ss3yQ4= github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 h1:9Qh4lJ/KMr5iS1zfZ8I97+3MDpiKjl+0lZVUNBhdvRs= github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08/go.mod h1:MAuu1uDJNOS3T3ui0qmKdPUwm59+bO19BbTph2wZafE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -121,16 +120,16 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/compose-spec/compose-go v1.7.0 h1:70HzJ/g81pdxF1ao9L7W2fgje/9FxNKH/davgvusEKc= github.com/compose-spec/compose-go v1.7.0/go.mod h1:Tb5Ae2PsYN3GTqYqzl2IRbTPiJtPZZjMw8UKUvmehFk= -github.com/containerd/stargz-snapshotter/estargz v0.13.0 h1:fD7AwuVV+B40p0d9qVkH/Au1qhp8hn/HWJHIYjpEcfw= -github.com/containerd/stargz-snapshotter/estargz v0.13.0/go.mod h1:m+9VaGJGlhCnrcEUod8mYumTmRgblwd3rC5UCEh2Yp0= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cppforlife/cobrautil v0.0.0-20200514214827-bb86e6965d72/go.mod h1:2w+qxVu2KSGW78Ex/XaIqfh/OvBgjEsmN53S4T8vEyA= -github.com/cppforlife/cobrautil v0.0.0-20221021151949-d60711905d65 h1:+3J1K6yQFRPKDEl5Py68c1q0FjaCkeMcB1nb7uzmpSw= -github.com/cppforlife/cobrautil v0.0.0-20221021151949-d60711905d65/go.mod h1:2w+qxVu2KSGW78Ex/XaIqfh/OvBgjEsmN53S4T8vEyA= +github.com/cppforlife/cobrautil v0.0.0-20221130162803-acdfead391ef h1:de10GNLe45JTMghl2qf9WH17H/BjGShK41X3vKAsPJA= +github.com/cppforlife/cobrautil v0.0.0-20221130162803-acdfead391ef/go.mod h1:2w+qxVu2KSGW78Ex/XaIqfh/OvBgjEsmN53S4T8vEyA= github.com/cppforlife/color v1.9.1-0.20200716202919-6706ac40b835 h1:mYQweUIBD+TBRjIeQnJmXr0GSVMpI6O0takyb/aaOgo= github.com/cppforlife/color v1.9.1-0.20200716202919-6706ac40b835/go.mod h1:dYeVsKp1vvK8XjdTPR1gF+uk+9doxKeO3hqQTOCr7T4= github.com/cppforlife/go-cli-ui v0.0.0-20200505234325-512793797f05/go.mod h1:I0qrzCmuPWYI6kAOvkllYjaW2aovclWbJ96+v+YyHb0= @@ -144,8 +143,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -155,14 +153,12 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/distribution/distribution/v3 v3.0.0-20221111170714-3b8fbf975279 h1:+lFUfSfK1/rMGIUUAwu6O+t4WGRwBU1EpaQTcN8KaeM= github.com/distribution/distribution/v3 v3.0.0-20221111170714-3b8fbf975279/go.mod h1:4x0IxAMsdeCSTr9UopCvp6MnryD2nyRLycsOrgvveAs= -github.com/docker/cli v20.10.24+incompatible h1:vfV+1kv9yD0/cpL6wWY9cE+Y9J8hL/NqJDGob0B3RVw= -github.com/docker/cli v20.10.24+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= +github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.25+incompatible h1:URiHXOEOlhi6FS5U+YUE8YnsnZjIV3R+TFezL2ngdW0= -github.com/docker/docker v20.10.25+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v23.0.3+incompatible h1:9GhVsShNWz1hO//9BNg/dpMnZW25KydO4wtVxWAIbho= +github.com/docker/docker v23.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= @@ -171,23 +167,18 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 h1:pEtiCjIXx3RvGjlUJuCNxNOw0MNblyR9Wi+vJGBFh+8= -github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= -github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= -github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= +github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -196,16 +187,14 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -230,8 +219,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= @@ -240,23 +230,32 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-containerregistry v0.12.1 h1:W1mzdNUTx4Zla4JaixCRLhORcR7G6KxE5hHl5fkPsp8= -github.com/google/go-containerregistry v0.12.1/go.mod h1:sdIK+oHQO7B93xI8UweYdl887YhuIwg9vz8BSLH3+8k= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.14.0 h1:z58vMqHxuwvAsVwvKEkmVBz2TlgBgH5k6koEXBtlYkw= +github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= github.com/google/safetext v0.0.0-20221026122733-23539d61753f h1:03r+JaAB8/2z83KOOCZK95tslx6e41NZS4Tpt569MtY= github.com/google/safetext v0.0.0-20221026122733-23539d61753f/go.mod h1:mJNEy0r5YPHC7ChQffpOszlGB4L1iqjXWpIEKcFpr9s= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -294,7 +293,6 @@ github.com/hpcloud/tail v1.0.1-0.20180514194441-a1dbeea552b7/go.mod h1:ab1qPbhIp github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -322,15 +320,15 @@ github.com/k14s/ytt v0.36.0/go.mod h1:awQ3bHBk1qT2Xn3GJVdmaLss2khZOIBBKFd2TNXZNM github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= -github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= @@ -338,24 +336,19 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= @@ -377,8 +370,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c h1:RC8WMpjonrBfyAh6VN/POIPtYD5tRAq0qMqCRjQNK+g= -github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c/go.mod h1:9OcmHNQQUTbk4XCffrLgN1NEKc2mh5u++biHVrvHsSU= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -392,28 +385,33 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU= +github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -431,19 +429,14 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= -github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= @@ -462,8 +455,8 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -472,12 +465,17 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -487,14 +485,16 @@ github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaW github.com/vito/go-interact v0.0.0-20171111012221-fa338ed9e9ec/go.mod h1:wPlfmglZmRWMYv/qJy3P+fK/UnoQB5ISk4txfNd9tDo= github.com/vito/go-interact v1.0.1 h1:O8xi8c93bRUv2Tb/v6HdiuGc+WnWt+AQzF74MOOdlBs= github.com/vito/go-interact v1.0.1/go.mod h1:HrdHSJXD2yn1MhlTwSIMeFgQ5WftiIorszVGd3S/DAA= -github.com/vmware-tanzu/carvel-imgpkg v0.33.0 h1:ZfMeJ+PKXJkkxcRSRiI/N9Vzxgyqj7YWktY3BEgQC84= -github.com/vmware-tanzu/carvel-imgpkg v0.33.0/go.mod h1:9J2sqH4lTws2fPhiloEWTc7tKvdsmmOxRqEWgMokTXM= -github.com/vmware-tanzu/carvel-kapp v0.53.0 h1:7FG7adc7K+nDMva9vFk9FzOVIhXgVlQJJ39CMN3wYec= -github.com/vmware-tanzu/carvel-kapp v0.53.0/go.mod h1:U2xQB7Bi7pLupnw8/gCH3xUnTQ0fhbjCLSd8GJ1WupY= -github.com/vmware-tanzu/carvel-kapp-controller v0.42.0 h1:Y+FISnGQMUU6L52uoAor0yu8DvmN7Kt4355+v/GbtJQ= -github.com/vmware-tanzu/carvel-kapp-controller v0.42.0/go.mod h1:TNGaklDZt7V4hYjU/YAlNPvsqDtDC+TGFRQuyGw8ykw= -github.com/vmware-tanzu/carvel-vendir v0.32.0 h1:3PtLspXVOdwCMaLE+WZTDtEhowoUSSvxnfXITxJLu9Q= -github.com/vmware-tanzu/carvel-vendir v0.32.0/go.mod h1:7TkdZo68KQK0XesY/+rvcedUOoFTqGP4gm4GpaVmZKg= +github.com/vmware-tanzu/carvel-imgpkg v0.37.2 h1:yGL38LG3+gue9kR3Zt4/3hEXPYtUPfAw4xgouRc8yTw= +github.com/vmware-tanzu/carvel-imgpkg v0.37.2/go.mod h1:ImU4bXbFMXa+FTuB72hkn3EfVkD0TVDrHy0HL83laMM= +github.com/vmware-tanzu/carvel-kapp v0.58.0 h1:Se3CXqPMh7M5z5TY/FdRVwni+TVYIepun/EcXQOZaKo= +github.com/vmware-tanzu/carvel-kapp v0.58.0/go.mod h1:zgDsmUCZZ62/Jso0k1aA2I3019ywcRWK8IEqeVQQDpc= +github.com/vmware-tanzu/carvel-kapp-controller v0.46.1 h1:mH3uQJqJnFbUwOPYng1xdJEsYxaOS7G1es39SOPOXd8= +github.com/vmware-tanzu/carvel-kapp-controller v0.46.1/go.mod h1:LJf4oaNfKW8kk6ddD8uRaayaUecGiqrz278oe4AEBSc= +github.com/vmware-tanzu/carvel-vendir v0.34.3 h1:l3w1fq1txvk4PIaoyNlS5oxHMzWXdiSiJHMUJ/w1S9A= +github.com/vmware-tanzu/carvel-vendir v0.34.3/go.mod h1:n3mFqV6UrLCwDy1S5Wr73XIbolTYfY+NOBqxC1ihjRk= +github.com/vmware-tanzu/carvel-ytt v0.45.3 h1:X314B23qfRcvxyoFF937ctkgX3vUhKSh/dcSld3mYqc= +github.com/vmware-tanzu/carvel-ytt v0.45.3/go.mod h1:oHqFBnn/JvqaUjcQo9T/a/WPUP1ituKjUpFPH+BTzfc= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -522,8 +522,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= -golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -546,8 +546,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180730214132-a0f8a16cb08c/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -568,15 +568,14 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU= -golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -584,9 +583,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -608,35 +606,31 @@ golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= @@ -661,10 +655,9 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -707,15 +700,15 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= @@ -745,23 +738,23 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.25.4 h1:3YO8J4RtmG7elEgaWMb4HgmpS2CfY1QlaOz9nwB+ZSs= -k8s.io/api v0.25.4/go.mod h1:IG2+RzyPQLllQxnhzD8KQNEu4c4YvyDTpSMztf4A0OQ= -k8s.io/apimachinery v0.25.4 h1:CtXsuaitMESSu339tfhVXhQrPET+EiWnIY1rcurKnAc= -k8s.io/apimachinery v0.25.4/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo= -k8s.io/client-go v0.25.4 h1:3RNRDffAkNU56M/a7gUfXaEzdhZlYhoW8dgViGy5fn8= -k8s.io/client-go v0.25.4/go.mod h1:8trHCAC83XKY0wsBIpbirZU4NTUpbuhc2JnI7OruGZw= -k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= -k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20220803164354-a70c9af30aea h1:3QOH5+2fGsY8e1qf+GIFpg+zw/JGNrgyZRQR7/m6uWg= -k8s.io/kube-openapi v0.0.0-20220803164354-a70c9af30aea/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= -k8s.io/kubectl v0.25.4 h1:O3OA1z4V1ZyvxCvScjq0pxAP7ABgznr8UvnVObgI6Dc= -k8s.io/kubectl v0.25.4/go.mod h1:CKMrQ67Bn2YCP26tZStPQGq62zr9pvzEf65A0navm8k= -k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2 h1:GfD9OzL11kvZN5iArC6oTS7RTj7oJOIfnislxYlqTj8= -k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.27.4 h1:0pCo/AN9hONazBKlNUdhQymmnfLRbSZjd5H5H3f0bSs= +k8s.io/api v0.27.4/go.mod h1:O3smaaX15NfxjzILfiln1D8Z3+gEYpjEpiNA/1EVK1Y= +k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs= +k8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= +k8s.io/client-go v0.27.4 h1:vj2YTtSJ6J4KxaC88P4pMPEQECWMY8gqPqsTgUKzvjk= +k8s.io/client-go v0.27.4/go.mod h1:ragcly7lUlN0SRPk5/ZkGnDjPknzb37TICq07WhI6Xc= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= +k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= +k8s.io/kubectl v0.27.4 h1:RV1TQLIbtL34+vIM+W7HaS3KfAbqvy9lWn6pWB9els4= +k8s.io/kubectl v0.27.4/go.mod h1:qtc1s3BouB9KixJkriZMQqTsXMc+OAni6FeKAhq7q14= +k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= +k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kind v0.17.0 h1:CScmGz/wX66puA06Gj8OZb76Wmk7JIjgWf5JDvY7msM= sigs.k8s.io/kind v0.17.0/go.mod h1:Qqp8AiwOlMZmJWs37Hgs31xcbiYXjtXlRBSftcnZXQk= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= diff --git a/client-programs/pkg/cluster/kindcluster.go b/client-programs/pkg/cluster/kindcluster.go index 86ec622b..3cd9987a 100644 --- a/client-programs/pkg/cluster/kindcluster.go +++ b/client-programs/pkg/cluster/kindcluster.go @@ -13,6 +13,7 @@ import ( "time" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/pkg/errors" "golang.org/x/exp/slices" @@ -131,18 +132,18 @@ func (o *KindClusterConfig) StopCluster() error { fmt.Println("Stopping cluster educates ...") - // timeout := 30 + timeout := 30 - // if err := cli.ContainerStop(ctx, "educates-control-plane", container.StopOptions{Timeout: &timeout}); err != nil { - // return errors.Wrapf(err, "failed to stop cluster") - // } - - timeout := time.Duration(30) * time.Second - - if err := cli.ContainerStop(ctx, "educates-control-plane", &timeout); err != nil { + if err := cli.ContainerStop(ctx, "educates-control-plane", container.StopOptions{Timeout: &timeout}); err != nil { return errors.Wrapf(err, "failed to stop cluster") } + // timeout := time.Duration(30) * time.Second + + // if err := cli.ContainerStop(ctx, "educates-control-plane", &timeout); err != nil { + // return errors.Wrapf(err, "failed to stop cluster") + // } + return nil } diff --git a/client-programs/pkg/cluster/kindclusterconfig.yaml.tpl b/client-programs/pkg/cluster/kindclusterconfig.yaml.tpl index dc6fe138..c81c47f8 100644 --- a/client-programs/pkg/cluster/kindclusterconfig.yaml.tpl +++ b/client-programs/pkg/cluster/kindclusterconfig.yaml.tpl @@ -39,7 +39,7 @@ nodes: {{- end }} containerdConfigPatches: - |- - [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.default.svc.cluster.local:5001"] + [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.default.svc.cluster.local"] endpoint = ["http://educates-registry:5000"] {{- if eq .ClusterSecurity.PolicyEngine "pod-security-standards" }} featureGates: diff --git a/client-programs/pkg/cmd/admin_cluster_create_cmd.go b/client-programs/pkg/cmd/admin_cluster_create_cmd.go index 1aba69d1..db7e9db2 100644 --- a/client-programs/pkg/cmd/admin_cluster_create_cmd.go +++ b/client-programs/pkg/cmd/admin_cluster_create_cmd.go @@ -183,6 +183,7 @@ func (o *AdminClusterCreateOptions) Run() error { ClusterSecurity: fullConfig.ClusterSecurity, ClusterRuntime: fullConfig.ClusterRuntime, ClusterIngress: fullConfig.ClusterIngress, + SessionCookies: fullConfig.SessionCookies, ClusterStorage: fullConfig.ClusterStorage, ClusterSecrets: fullConfig.ClusterSecrets, TrainingPortal: fullConfig.TrainingPortal, diff --git a/client-programs/pkg/cmd/admin_platform_config_cmd.go b/client-programs/pkg/cmd/admin_platform_config_cmd.go index 359d5ef2..bfd7239b 100644 --- a/client-programs/pkg/cmd/admin_platform_config_cmd.go +++ b/client-programs/pkg/cmd/admin_platform_config_cmd.go @@ -140,6 +140,7 @@ func (o *AdminPlatformConfigUpdateOptions) Run() error { ClusterSecurity: fullConfig.ClusterSecurity, ClusterRuntime: fullConfig.ClusterRuntime, ClusterIngress: fullConfig.ClusterIngress, + SessionCookies: fullConfig.SessionCookies, ClusterStorage: fullConfig.ClusterStorage, ClusterSecrets: fullConfig.ClusterSecrets, TrainingPortal: fullConfig.TrainingPortal, diff --git a/client-programs/pkg/cmd/admin_platform_delete_cmd.go b/client-programs/pkg/cmd/admin_platform_delete_cmd.go index b5292e6a..a846699c 100644 --- a/client-programs/pkg/cmd/admin_platform_delete_cmd.go +++ b/client-programs/pkg/cmd/admin_platform_delete_cmd.go @@ -28,6 +28,7 @@ func (o *AdminPlatformDeleteOptions) Run() error { ClusterSecurity: fullConfig.ClusterSecurity, ClusterRuntime: fullConfig.ClusterRuntime, ClusterIngress: fullConfig.ClusterIngress, + SessionCookies: fullConfig.SessionCookies, ClusterStorage: fullConfig.ClusterStorage, ClusterSecrets: fullConfig.ClusterSecrets, TrainingPortal: fullConfig.TrainingPortal, diff --git a/client-programs/pkg/cmd/admin_platform_deploy_cmd.go b/client-programs/pkg/cmd/admin_platform_deploy_cmd.go index 2d2effa9..26ede5fd 100644 --- a/client-programs/pkg/cmd/admin_platform_deploy_cmd.go +++ b/client-programs/pkg/cmd/admin_platform_deploy_cmd.go @@ -52,6 +52,7 @@ func (o *AdminPlatformDeployOptions) Run() error { ClusterSecurity: fullConfig.ClusterSecurity, ClusterRuntime: fullConfig.ClusterRuntime, ClusterIngress: fullConfig.ClusterIngress, + SessionCookies: fullConfig.SessionCookies, ClusterStorage: fullConfig.ClusterStorage, ClusterSecrets: fullConfig.ClusterSecrets, TrainingPortal: fullConfig.TrainingPortal, diff --git a/client-programs/pkg/cmd/cluster_portal_create_cmd.go b/client-programs/pkg/cmd/cluster_portal_create_cmd.go index eb735445..df4be678 100644 --- a/client-programs/pkg/cmd/cluster_portal_create_cmd.go +++ b/client-programs/pkg/cmd/cluster_portal_create_cmd.go @@ -15,10 +15,12 @@ import ( ) type ClusterConfigViewOptions struct { - Kubeconfig string - Portal string - Capacity uint - Password string + Kubeconfig string + Portal string + Capacity uint + Password string + ThemeName string + CookieDomain string } func (o *ClusterConfigViewOptions) Run(isPasswordSet bool) error { @@ -40,7 +42,7 @@ func (o *ClusterConfigViewOptions) Run(isPasswordSet bool) error { // Update the training portal, creating it if necessary. - err = createTrainingPortal(dynamicClient, o.Portal, o.Capacity, o.Password, isPasswordSet) + err = createTrainingPortal(dynamicClient, o.Portal, o.Capacity, o.Password, isPasswordSet, o.ThemeName, o.CookieDomain) if err != nil { return err @@ -88,11 +90,23 @@ func (p *ProjectInfo) NewClusterPortalCreateCmd() *cobra.Command { "", "override password for training portal access", ) + c.Flags().StringVar( + &o.ThemeName, + "theme-name", + "", + "override theme used by training portal and workshops", + ) + c.Flags().StringVar( + &o.CookieDomain, + "cookie-domain", + "", + "override cookie domain used by training portal and workshops", + ) return c } -func createTrainingPortal(client dynamic.Interface, portal string, capacity uint, password string, isPasswordSet bool) error { +func createTrainingPortal(client dynamic.Interface, portal string, capacity uint, password string, isPasswordSet bool, themeName string, cookieDomain string) error { trainingPortalClient := client.Resource(trainingPortalResource) _, err := trainingPortalClient.Get(context.TODO(), portal, metav1.GetOptions{}) @@ -142,6 +156,16 @@ func createTrainingPortal(client dynamic.Interface, portal string, capacity uint Reserved: 0, }, }, + "theme": struct { + Name string `json:"name"` + }{ + Name: themeName, + }, + "cookies": struct { + Domain string `json:"domain"` + }{ + Domain: cookieDomain, + }, }, "workshops": []interface{}{}, }, diff --git a/client-programs/pkg/cmd/cluster_session_list_cmd.go b/client-programs/pkg/cmd/cluster_session_list_cmd.go index 0a8230d4..f0f0f1aa 100644 --- a/client-programs/pkg/cmd/cluster_session_list_cmd.go +++ b/client-programs/pkg/cmd/cluster_session_list_cmd.go @@ -38,7 +38,7 @@ func (o *ClusterSessionListOptions) Run() error { workshopSessionClient := dynamicClient.Resource(workshopSessionResource) - trainingPortals, err := workshopSessionClient.List(context.TODO(), metav1.ListOptions{}) + workshopSessions, err := workshopSessionClient.List(context.TODO(), metav1.ListOptions{}) if k8serrors.IsNotFound(err) { fmt.Println("No sessions found.") @@ -47,7 +47,7 @@ func (o *ClusterSessionListOptions) Run() error { var sessions []unstructured.Unstructured - for _, item := range trainingPortals.Items { + for _, item := range workshopSessions.Items { labels := item.GetLabels() portal, ok := labels["training.educates.dev/portal.name"] diff --git a/client-programs/pkg/cmd/cluster_workshop_cmd_group.go b/client-programs/pkg/cmd/cluster_workshop_cmd_group.go index 77fc7df1..2c18f0b3 100644 --- a/client-programs/pkg/cmd/cluster_workshop_cmd_group.go +++ b/client-programs/pkg/cmd/cluster_workshop_cmd_group.go @@ -24,6 +24,7 @@ func (p *ProjectInfo) NewClusterWorkshopCmdGroup() *cobra.Command { Commands: []*cobra.Command{ p.NewClusterWorkshopDeployCmd(), p.NewClusterWorkshopListCmd(), + p.NewClusterWorkshopServeCmd(), p.NewClusterWorkshopRequestCmd(), p.NewClusterWorkshopUpdateCmd(), p.NewClusterWorkshopDeleteCmd(), diff --git a/client-programs/pkg/cmd/cluster_workshop_delete_cmd.go b/client-programs/pkg/cmd/cluster_workshop_delete_cmd.go index 239c686f..31afb7d4 100644 --- a/client-programs/pkg/cmd/cluster_workshop_delete_cmd.go +++ b/client-programs/pkg/cmd/cluster_workshop_delete_cmd.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/vmware-tanzu-labs/educates-training-platform/client-programs/pkg/cluster" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -15,10 +16,13 @@ import ( ) type ClusterWorkshopDeleteOptions struct { - Name string - Path string - Kubeconfig string - Portal string + Name string + Path string + Kubeconfig string + Portal string + WorkshopFile string + WorkshopVersion string + DataValuesFlags yttcmd.DataValuesFlags } func (o *ClusterWorkshopDeleteOptions) Run() error { @@ -49,7 +53,7 @@ func (o *ClusterWorkshopDeleteOptions) Run() error { var workshop *unstructured.Unstructured - if workshop, err = loadWorkshopDefinition(o.Name, path, o.Portal); err != nil { + if workshop, err = loadWorkshopDefinition(o.Name, path, o.Portal, o.WorkshopFile, o.WorkshopVersion, o.DataValuesFlags); err != nil { return err } @@ -113,6 +117,58 @@ func (p *ProjectInfo) NewClusterWorkshopDeleteCmd() *cobra.Command { "name to be used for training portal and workshop name prefixes", ) + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop being published", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + return c } diff --git a/client-programs/pkg/cmd/cluster_workshop_deploy_cmd.go b/client-programs/pkg/cmd/cluster_workshop_deploy_cmd.go index c0cbd108..a33f1143 100644 --- a/client-programs/pkg/cmd/cluster_workshop_deploy_cmd.go +++ b/client-programs/pkg/cmd/cluster_workshop_deploy_cmd.go @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/vmware-tanzu-labs/educates-training-platform/client-programs/pkg/cluster" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -20,19 +21,23 @@ import ( ) type ClusterWorkshopDeployOptions struct { - Name string - Path string - Kubeconfig string - Portal string - Capacity uint - Reserved uint - Initial uint - Expires string - Overtime string - Deadline string - Orphaned string - Overdue string - Environ []string + Name string + Path string + Kubeconfig string + Portal string + Capacity uint + Reserved uint + Initial uint + Expires string + Overtime string + Deadline string + Orphaned string + Overdue string + Refresh string + Environ []string + WorkshopFile string + WorkshopVersion string + DataValuesFlags yttcmd.DataValuesFlags } func (o *ClusterWorkshopDeployOptions) Run() error { @@ -60,7 +65,7 @@ func (o *ClusterWorkshopDeployOptions) Run() error { var workshop *unstructured.Unstructured - if workshop, err = loadWorkshopDefinition(o.Name, path, o.Portal); err != nil { + if workshop, err = loadWorkshopDefinition(o.Name, path, o.Portal, o.WorkshopFile, o.WorkshopVersion, o.DataValuesFlags); err != nil { return err } @@ -82,7 +87,7 @@ func (o *ClusterWorkshopDeployOptions) Run() error { // Update the training portal, creating it if necessary. - err = deployWorkshopResource(dynamicClient, workshop, o.Portal, o.Capacity, o.Reserved, o.Initial, o.Expires, o.Overtime, o.Deadline, o.Orphaned, o.Overdue, o.Environ) + err = deployWorkshopResource(dynamicClient, workshop, o.Portal, o.Capacity, o.Reserved, o.Initial, o.Expires, o.Overtime, o.Deadline, o.Orphaned, o.Overdue, o.Refresh, o.Environ) if err != nil { return err @@ -176,6 +181,12 @@ func (p *ProjectInfo) NewClusterWorkshopDeployCmd() *cobra.Command { "2m", "allowed startup time before workshop is deemed failed", ) + c.Flags().StringVar( + &o.Refresh, + "refresh", + "", + "interval after which workshop environment is recreated", + ) c.Flags().StringSliceVarP( &o.Environ, "env", @@ -184,12 +195,64 @@ func (p *ProjectInfo) NewClusterWorkshopDeployCmd() *cobra.Command { "environment variable overrides for workshop", ) + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop being published", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + return c } var trainingPortalResource = schema.GroupVersionResource{Group: "training.educates.dev", Version: "v1beta1", Resource: "trainingportals"} -func deployWorkshopResource(client dynamic.Interface, workshop *unstructured.Unstructured, portal string, capacity uint, reserved uint, initial uint, expires string, overtime string, deadline string, orphaned string, overdue string, environ []string) error { +func deployWorkshopResource(client dynamic.Interface, workshop *unstructured.Unstructured, portal string, capacity uint, reserved uint, initial uint, expires string, overtime string, deadline string, orphaned string, overdue string, refresh string, environ []string) error { trainingPortalClient := client.Resource(trainingPortalResource) trainingPortal, err := trainingPortalClient.Get(context.TODO(), portal, metav1.GetOptions{}) @@ -352,6 +415,12 @@ func deployWorkshopResource(client dynamic.Interface, workshop *unstructured.Uns delete(object, "overdue") } + if refresh != "" { + object["refresh"] = refresh + } else { + delete(object, "refresh") + } + var tmpEnvironVariables []interface{} for _, item := range environVariables { @@ -375,6 +444,7 @@ func deployWorkshopResource(client dynamic.Interface, workshop *unstructured.Uns Deadline string `json:"deadline,omitempty"` Orphaned string `json:"orphaned,omitempty"` Overdue string `json:"overdue,omitempty"` + Refresh string `json:"refresh,omitempty"` Environ []EnvironDetails `json:"env"` } @@ -388,6 +458,7 @@ func deployWorkshopResource(client dynamic.Interface, workshop *unstructured.Uns Deadline: deadline, Orphaned: orphaned, Overdue: overdue, + Refresh: refresh, Environ: environVariables, } diff --git a/client-programs/pkg/cmd/cluster_workshop_request_cmd.go b/client-programs/pkg/cmd/cluster_workshop_request_cmd.go index f82e776f..57e3d3e7 100644 --- a/client-programs/pkg/cmd/cluster_workshop_request_cmd.go +++ b/client-programs/pkg/cmd/cluster_workshop_request_cmd.go @@ -9,9 +9,9 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "net/url" + "os" "os/exec" "runtime" "strings" @@ -20,6 +20,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/vmware-tanzu-labs/educates-training-platform/client-programs/pkg/cluster" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -27,14 +28,17 @@ import ( ) type ClusterWorkshopRequestOptions struct { - Name string - Path string - Kubeconfig string - Portal string - Params []string - ParamFiles []string - ParamsFiles []string - IndexUrl string + Name string + Path string + Kubeconfig string + Portal string + Params []string + ParamFiles []string + ParamsFiles []string + IndexUrl string + WorkshopFile string + WorkshopVersion string + DataValuesFlags yttcmd.DataValuesFlags } func (o *ClusterWorkshopRequestOptions) Run() error { @@ -63,7 +67,7 @@ func (o *ClusterWorkshopRequestOptions) Run() error { return errors.Errorf("invalid parameter format %s", item) } - content, err := ioutil.ReadFile(parts[1]) + content, err := os.ReadFile(parts[1]) if err != nil { return errors.Wrapf(err, "cannot read parameter data file %s", parts[1]) @@ -109,7 +113,7 @@ func (o *ClusterWorkshopRequestOptions) Run() error { var workshop *unstructured.Unstructured - if workshop, err = loadWorkshopDefinition(o.Name, path, o.Portal); err != nil { + if workshop, err = loadWorkshopDefinition(o.Name, path, o.Portal, o.WorkshopFile, o.WorkshopVersion, o.DataValuesFlags); err != nil { return err } @@ -200,11 +204,62 @@ func (p *ProjectInfo) NewClusterWorkshopRequestCmd() *cobra.Command { "the URL to redirect to when workshop session is complete", ) + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop being published", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + return c } func requestWorkshop(client dynamic.Interface, name string, portal string, params map[string]string, indexUrl string) error { - trainingPortalClient := client.Resource(trainingPortalResource) trainingPortal, err := trainingPortalClient.Get(context.TODO(), portal, metav1.GetOptions{}) diff --git a/client-programs/pkg/cmd/cluster_workshop_serve_cmd.go b/client-programs/pkg/cmd/cluster_workshop_serve_cmd.go new file mode 100644 index 00000000..b9182920 --- /dev/null +++ b/client-programs/pkg/cmd/cluster_workshop_serve_cmd.go @@ -0,0 +1,325 @@ +// Copyright 2022-2023 The Educates Authors. + +package cmd + +import ( + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/vmware-tanzu-labs/educates-training-platform/client-programs/pkg/cluster" + "github.com/vmware-tanzu-labs/educates-training-platform/client-programs/pkg/renderer" +) + +func calculateWorkshopRoot(path string) (string, error) { + var err error + + // If path not provided assume the current working directory. + + if path == "" { + path = "." + } + + path = filepath.Clean(path) + + if path, err = filepath.Abs(path); err != nil { + return "", errors.Wrap(err, "couldn't convert workshop directory to absolute path") + } + + fileInfo, err := os.Stat(path) + + if err != nil || !fileInfo.IsDir() { + return "", errors.New("workshop directory does not exist or path is not a directory") + } + + return path, nil +} + +// func calculateWorkshopName(name string, path string, portal string, workshopFile string, workshopVersion string, dataValuesFlags yttcmd.DataValuesFlags) (string, error) { +// var err error + +// if name == "" { +// var workshop *unstructured.Unstructured + +// if workshop, err = loadWorkshopDefinition(name, path, portal, workshopFile, workshopVersion, dataValuesFlags); err != nil { +// return "", err +// } + +// name = workshop.GetName() +// } + +// return name, nil +// } + +type ClusterWorkshopServeOptions struct { + Name string + Path string + Kubeconfig string + Portal string + ProxyProtocol string + ProxyHost string + ProxyPort int + LocalHost string + LocalPort int + HugoPort int + Token string + Files bool + WorkshopFile string + WorkshopVersion string + PatchWorkshop bool + DataValuesFlags yttcmd.DataValuesFlags +} + +func (o *ClusterWorkshopServeOptions) Run() error { + var err error + + var name = o.Name + var path = o.Path + var portal = o.Portal + var token = o.Token + + // Ensure have portal name. + + if portal == "" { + portal = "educates-cli" + } + + // Calculate workshop root and name. + + if path, err = calculateWorkshopRoot(path); err != nil { + return err + } + + var workshop *unstructured.Unstructured + + if workshop, err = loadWorkshopDefinition(name, path, portal, o.WorkshopFile, o.WorkshopVersion, o.DataValuesFlags); err != nil { + return err + } + + if name == "" { + name = workshop.GetName() + } + + // If going to patch hosted workshop, ensure we have an access token. + + if o.PatchWorkshop && token == "" { + token = randomPassword(16) + } + + // If patching hosted workshop create an apply the updated configuration. + + if o.PatchWorkshop { + patchedWorkshop := workshop.DeepCopyObject().(*unstructured.Unstructured) + + proxyDefinition := map[string]interface{}{ + "enabled": true, + "proxy": map[string]interface{}{ + "protocol": o.ProxyProtocol, + "host": o.ProxyHost, + "port": int64(o.ProxyPort), + "changeOrigin": false, + "headers": []interface{}{ + map[string]interface{}{ + "name": "X-Session-Name", + "value": "$(session_name)", + }, + map[string]interface{}{ + "name": "X-Access-Token", + "value": token, + }, + }, + }, + } + + unstructured.SetNestedField(patchedWorkshop.Object, proxyDefinition, "spec", "session", "applications", "workshop") + + clusterConfig := cluster.NewClusterConfig(o.Kubeconfig) + + dynamicClient, err := clusterConfig.GetDynamicClient() + + if err != nil { + return errors.Wrapf(err, "unable to create Kubernetes client") + } + + // Update the workshop resource in the Kubernetes cluster. + + err = updateWorkshopResource(dynamicClient, patchedWorkshop) + + if err != nil { + return err + } + } + + var cleanupFunc = func() { + // Do our best to revert workshop configuration and ignore errors. + + clusterConfig := cluster.NewClusterConfig(o.Kubeconfig) + + dynamicClient, err := clusterConfig.GetDynamicClient() + + if err == nil { + // Update the workshop resource in the Kubernetes cluster. + + updateWorkshopResource(dynamicClient, workshop) + } + } + + // Run the proxy server and Hugo server. + + return renderer.RunHugoServer(path, o.Kubeconfig, name, portal, o.LocalHost, o.LocalPort, o.HugoPort, token, o.Files, cleanupFunc) +} + +func (p *ProjectInfo) NewClusterWorkshopServeCmd() *cobra.Command { + var o ClusterWorkshopServeOptions + + var c = &cobra.Command{ + Args: cobra.NoArgs, + Use: "serve", + Short: "Serve workshop from local system", + RunE: func(_ *cobra.Command, _ []string) error { return o.Run() }, + } + + c.Flags().StringVarP( + &o.Name, + "name", + "n", + "", + "name to be used for the workshop definition, generated if not set", + ) + c.Flags().StringVarP( + &o.Path, + "file", + "f", + ".", + "path to local workshop directory", + ) + c.Flags().StringVar( + &o.Kubeconfig, + "kubeconfig", + "", + "kubeconfig file to use instead of $KUBECONFIG or $HOME/.kube/config", + ) + c.Flags().StringVarP( + &o.Portal, + "portal", + "p", + "educates-cli", + "name of the training portal to lookup the workshop", + ) + c.Flags().StringVar( + &o.ProxyProtocol, + "proxy-protocol", + "http", + "protocol by which any remote proxy will be accessed", + ) + c.Flags().StringVar( + &o.ProxyHost, + "proxy-host", + "localhost.$(ingress_domain)", + "host by which any remote proxy will be accessed", + ) + c.Flags().IntVar( + &o.ProxyPort, + "proxy-port", + 10081, + "port on which any remote proxy service will listen", + ) + c.Flags().StringVar( + &o.LocalHost, + "local-host", + "0.0.0.0", + "host on which the local proxy will be listen", + ) + c.Flags().IntVar( + &o.LocalPort, + "local-port", + 10081, + "port on which the local proxy will listen", + ) + c.Flags().IntVar( + &o.HugoPort, + "hugo-port", + 1313, + "port on which the hugo server will listen", + ) + c.Flags().StringVarP( + &o.Token, + "access-token", + "", + "", + "access token for protecting access to server", + ) + c.Flags().BoolVarP( + &o.Files, + "allow-files-download", + "", + false, + "enable download of workshop files as tarball", + ) + + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop being published", + ) + + c.Flags().BoolVarP( + &o.PatchWorkshop, + "patch-workshop", + "", + false, + "Patch hosted workshop to proxy sessions to local server ", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + + return c +} diff --git a/client-programs/pkg/cmd/cluster_workshop_update_cmd.go b/client-programs/pkg/cmd/cluster_workshop_update_cmd.go index 86f39d96..0db67be0 100644 --- a/client-programs/pkg/cmd/cluster_workshop_update_cmd.go +++ b/client-programs/pkg/cmd/cluster_workshop_update_cmd.go @@ -15,6 +15,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/vmware-tanzu-labs/educates-training-platform/client-programs/pkg/cluster" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -26,10 +27,13 @@ import ( ) type ClusterWorkshopUpdateOptions struct { - Name string - Path string - Kubeconfig string - Portal string + Name string + Path string + Kubeconfig string + Portal string + WorkshopFile string + WorkshopVersion string + DataValuesFlags yttcmd.DataValuesFlags } func (o *ClusterWorkshopUpdateOptions) Run() error { @@ -57,7 +61,7 @@ func (o *ClusterWorkshopUpdateOptions) Run() error { var workshop *unstructured.Unstructured - if workshop, err = loadWorkshopDefinition(o.Name, path, o.Portal); err != nil { + if workshop, err = loadWorkshopDefinition(o.Name, path, o.Portal, o.WorkshopFile, o.WorkshopVersion, o.DataValuesFlags); err != nil { return err } @@ -118,10 +122,62 @@ func (p *ProjectInfo) NewClusterWorkshopUpdateCmd() *cobra.Command { "name to be used for training portal and workshop name prefixes", ) + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop being published", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + return c } -func loadWorkshopDefinition(name string, path string, portal string) (*unstructured.Unstructured, error) { +func loadWorkshopDefinition(name string, path string, portal string, workshopFile string, workshopVersion string, dataValueFlags yttcmd.DataValuesFlags) (*unstructured.Unstructured, error) { // Parse the workshop location so we can determine if it is a local file // or accessible using a HTTP/HTTPS URL. @@ -134,7 +190,7 @@ func loadWorkshopDefinition(name string, path string, portal string) (*unstructu // Check if file system path first (not HTTP/HTTPS) and if so normalize // the path. If it the path references a directory, then extend the path - // so we look for the resources/workshop.yaml file within that directory. + // so we look for the workshop file within that directory. if urlInfo.Scheme != "http" && urlInfo.Scheme != "https" { path = filepath.Clean(path) @@ -143,14 +199,18 @@ func loadWorkshopDefinition(name string, path string, portal string) (*unstructu return nil, errors.Wrap(err, "couldn't convert workshop location to absolute path") } - fileInfo, err := os.Stat(path) + if !filepath.IsAbs(workshopFile) { + fileInfo, err := os.Stat(path) - if err != nil { - return nil, errors.Wrap(err, "couldn't test if workshop location is a directory") - } + if err != nil { + return nil, errors.Wrap(err, "couldn't test if workshop location is a directory") + } - if fileInfo.IsDir() { - path = filepath.Join(path, "resources", "workshop.yaml") + if fileInfo.IsDir() { + path = filepath.Join(path, workshopFile) + } + } else { + path = workshopFile } } @@ -184,6 +244,12 @@ func loadWorkshopDefinition(name string, path string, portal string) (*unstructu } } + // Process the workshop YAML data in case it contains ytt templating. + + if workshopData, err = processWorkshopDefinition(workshopData, dataValueFlags); err != nil { + return nil, errors.Wrap(err, "unable to process workshop definition as template") + } + // Parse the workshop definition. decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDecoder() @@ -229,6 +295,18 @@ func loadWorkshopDefinition(name string, path string, portal string) (*unstructu workshop.SetName(name) + // Insert workshop version property if not specified. + + _, found, _ := unstructured.NestedString(workshop.Object, "spec", "version") + + if !found && workshopVersion != "latest" { + unstructured.SetNestedField(workshop.Object, workshopVersion, "spec", "version") + } + + // Remove the publish section as will not be accurate after publising. + + unstructured.RemoveNestedField(workshop.Object, "spec", "publish") + return workshop, nil } diff --git a/client-programs/pkg/cmd/docker_workshop_delete_cmd.go b/client-programs/pkg/cmd/docker_workshop_delete_cmd.go index 741cd4dc..75488f41 100644 --- a/client-programs/pkg/cmd/docker_workshop_delete_cmd.go +++ b/client-programs/pkg/cmd/docker_workshop_delete_cmd.go @@ -13,12 +13,16 @@ import ( "github.com/docker/docker/client" "github.com/pkg/errors" "github.com/spf13/cobra" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) type DockerWorkshopDeleteOptions struct { - Name string - Path string + Name string + Path string + WorkshopFile string + WorkshopVersion string + DataValuesFlags yttcmd.DataValuesFlags } func (o *DockerWorkshopDeleteOptions) Run(cmd *cobra.Command) error { @@ -43,7 +47,7 @@ func (o *DockerWorkshopDeleteOptions) Run(cmd *cobra.Command) error { var workshop *unstructured.Unstructured - if workshop, err = loadWorkshopDefinition(o.Name, path, "educates-cli"); err != nil { + if workshop, err = loadWorkshopDefinition(o.Name, path, "educates-cli", o.WorkshopFile, o.WorkshopVersion, o.DataValuesFlags); err != nil { return err } @@ -119,5 +123,57 @@ func (p *ProjectInfo) NewDockerWorkshopDeleteCmd() *cobra.Command { "path to local workshop directory, definition file, or URL for workshop definition file", ) + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop being published", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + return c } diff --git a/client-programs/pkg/cmd/docker_workshop_deploy_cmd.go b/client-programs/pkg/cmd/docker_workshop_deploy_cmd.go index 4337e5d2..b2b6c2da 100644 --- a/client-programs/pkg/cmd/docker_workshop_deploy_cmd.go +++ b/client-programs/pkg/cmd/docker_workshop_deploy_cmd.go @@ -23,6 +23,7 @@ import ( "github.com/docker/docker/client" "github.com/pkg/errors" "github.com/spf13/cobra" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" "golang.org/x/exp/slices" "gopkg.in/yaml.v2" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -42,6 +43,9 @@ type DockerWorkshopDeployOptions struct { Cluster string KubeConfig string Assets string + WorkshopFile string + WorkshopVersion string + DataValuesFlags yttcmd.DataValuesFlags } const containerScript = `exec bash -s << "EOF" @@ -100,7 +104,7 @@ func (o *DockerWorkshopDeployOptions) Run(cmd *cobra.Command) error { var workshop *unstructured.Unstructured - if workshop, err = loadWorkshopDefinition("", o.Path, "educates-cli"); err != nil { + if workshop, err = loadWorkshopDefinition("", o.Path, "educates-cli", o.WorkshopFile, o.WorkshopVersion, o.DataValuesFlags); err != nil { return err } @@ -193,15 +197,15 @@ func (o *DockerWorkshopDeployOptions) Run(cmd *cobra.Command) error { return err } - if vendirFilesConfigData, err = generateVendirFilesConfig(workshop, originalName, o.Repository); err != nil { + if vendirFilesConfigData, err = generateVendirFilesConfig(workshop, originalName, o.Repository, o.WorkshopVersion); err != nil { return err } - if vendirPackagesConfigData, err = generateVendirPackagesConfig(workshop, originalName, o.Repository); err != nil { + if vendirPackagesConfigData, err = generateVendirPackagesConfig(workshop, originalName, o.Repository, o.WorkshopVersion); err != nil { return err } - if workshopImageName, err = generateWorkshopImageName(workshop, o.Repository, o.Version); err != nil { + if workshopImageName, err = generateWorkshopImageName(workshop, o.Repository, o.Version, o.WorkshopVersion); err != nil { return err } @@ -460,7 +464,7 @@ func (p *ProjectInfo) NewDockerWorkshopDeployCmd() *cobra.Command { ) c.Flags().StringVar( &o.Repository, - "repository", + "image-repository", "localhost:5001", "the address of the image repository", ) @@ -472,7 +476,7 @@ func (p *ProjectInfo) NewDockerWorkshopDeployCmd() *cobra.Command { ) c.Flags().StringVar( &o.Version, - "version", + "image-version", p.Version, "version of workshop base images to be used", ) @@ -495,13 +499,65 @@ func (p *ProjectInfo) NewDockerWorkshopDeployCmd() *cobra.Command { "local directory path to workshop assets", ) + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop definition", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + return c } func generateWorkshopConfig(workshop *unstructured.Unstructured) (string, error) { - workshopTitle, _, _ := unstructured.NestedMap(workshop.Object, "spec", "title") - workshopDescription, _, _ := unstructured.NestedMap(workshop.Object, "spec", "description") - applicationsConfig, _, _ := unstructured.NestedMap(workshop.Object, "spec", "session", "applications") + workshopTitle, _, _ := unstructured.NestedFieldNoCopy(workshop.Object, "spec", "title") + workshopDescription, _, _ := unstructured.NestedFieldNoCopy(workshop.Object, "spec", "description") + applicationsConfig, _, _ := unstructured.NestedFieldNoCopy(workshop.Object, "spec", "session", "applications") ingressesConfig, _, _ := unstructured.NestedSlice(workshop.Object, "spec", "session", "ingresses") dashboardsConfig, _, _ := unstructured.NestedSlice(workshop.Object, "spec", "session", "dashboards") @@ -526,9 +582,15 @@ func generateWorkshopConfig(workshop *unstructured.Unstructured) (string, error) return string(workshopConfigData), nil } -func generateVendirFilesConfig(workshop *unstructured.Unstructured, name string, repository string) ([]string, error) { +func generateVendirFilesConfig(workshop *unstructured.Unstructured, name string, repository string, version string) ([]string, error) { var vendirConfigs []string + workshopVersion, found, _ := unstructured.NestedString(workshop.Object, "spec", "version") + + if !found { + workshopVersion = version + } + filesItems, found, _ := unstructured.NestedSlice(workshop.Object, "spec", "workshop", "files") if found && len(filesItems) != 0 { @@ -570,6 +632,7 @@ func generateVendirFilesConfig(workshop *unstructured.Unstructured, name string, vendirConfigString = strings.ReplaceAll(vendirConfigString, "$(image_repository)", repository) vendirConfigString = strings.ReplaceAll(vendirConfigString, "$(workshop_name)", name) + vendirConfigString = strings.ReplaceAll(vendirConfigString, "$(workshop_version)", workshopVersion) vendirConfigs = append(vendirConfigs, vendirConfigString) } @@ -578,9 +641,15 @@ func generateVendirFilesConfig(workshop *unstructured.Unstructured, name string, return vendirConfigs, nil } -func generateVendirPackagesConfig(workshop *unstructured.Unstructured, name string, repository string) (string, error) { +func generateVendirPackagesConfig(workshop *unstructured.Unstructured, name string, repository string, version string) (string, error) { var vendirConfigString string + workshopVersion, found, _ := unstructured.NestedString(workshop.Object, "spec", "version") + + if !found { + workshopVersion = version + } + packagesItems, found, _ := unstructured.NestedSlice(workshop.Object, "spec", "workshop", "packages") if found && len(packagesItems) != 0 { @@ -634,12 +703,19 @@ func generateVendirPackagesConfig(workshop *unstructured.Unstructured, name stri vendirConfigString = strings.ReplaceAll(vendirConfigString, "$(image_repository)", repository) vendirConfigString = strings.ReplaceAll(vendirConfigString, "$(workshop_name)", name) + vendirConfigString = strings.ReplaceAll(vendirConfigString, "$(workshop_version)", workshopVersion) } return vendirConfigString, nil } -func generateWorkshopImageName(workshop *unstructured.Unstructured, repository string, version string) (string, error) { +func generateWorkshopImageName(workshop *unstructured.Unstructured, repository string, baseImageVersion string, workshopVersion string) (string, error) { + _, found, _ := unstructured.NestedString(workshop.Object, "spec", "version") + + if found { + workshopVersion, _, _ = unstructured.NestedString(workshop.Object, "spec", "version") + } + image, found, err := unstructured.NestedString(workshop.Object, "spec", "workshop", "image") if err != nil { @@ -650,7 +726,7 @@ func generateWorkshopImageName(workshop *unstructured.Unstructured, repository s image = "base-environment:*" } - defaultImageVersion := strings.TrimSpace(version) + defaultImageVersion := strings.TrimSpace(baseImageVersion) image = strings.ReplaceAll(image, "base-environment:*", fmt.Sprintf("ghcr.io/vmware-tanzu-labs/educates-base-environment:%s", defaultImageVersion)) image = strings.ReplaceAll(image, "jdk8-environment:*", fmt.Sprintf("ghcr.io/vmware-tanzu-labs/educates-jdk8-environment:%s", defaultImageVersion)) @@ -659,6 +735,7 @@ func generateWorkshopImageName(workshop *unstructured.Unstructured, repository s image = strings.ReplaceAll(image, "conda-environment:*", fmt.Sprintf("ghcr.io/vmware-tanzu-labs/educates-conda-environment:%s", defaultImageVersion)) image = strings.ReplaceAll(image, "$(image_repository)", repository) + image = strings.ReplaceAll(image, "$(workshop_version)", workshopVersion) return image, nil } diff --git a/client-programs/pkg/cmd/docker_workshop_logs.go b/client-programs/pkg/cmd/docker_workshop_logs.go index d8d29300..682c883b 100644 --- a/client-programs/pkg/cmd/docker_workshop_logs.go +++ b/client-programs/pkg/cmd/docker_workshop_logs.go @@ -7,13 +7,17 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) type DockerWorkshopLogsOptions struct { - Name string - Path string - Follow bool + Name string + Path string + Follow bool + WorkshopFile string + WorkshopVersion string + DataValuesFlags yttcmd.DataValuesFlags } func (o *DockerWorkshopLogsOptions) Run(cmd *cobra.Command) error { @@ -38,7 +42,7 @@ func (o *DockerWorkshopLogsOptions) Run(cmd *cobra.Command) error { var workshop *unstructured.Unstructured - if workshop, err = loadWorkshopDefinition(o.Name, path, "educates-cli"); err != nil { + if workshop, err = loadWorkshopDefinition(o.Name, path, "educates-cli", o.WorkshopFile, o.WorkshopVersion, o.DataValuesFlags); err != nil { return err } @@ -101,5 +105,57 @@ func (p *ProjectInfo) NewDockerWorkshopLogsCmd() *cobra.Command { "specify if the logs should be streamed", ) + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop being published", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + return c } diff --git a/client-programs/pkg/cmd/docker_workshop_open_cmd.go b/client-programs/pkg/cmd/docker_workshop_open_cmd.go index fbe0301f..ce2d3e05 100644 --- a/client-programs/pkg/cmd/docker_workshop_open_cmd.go +++ b/client-programs/pkg/cmd/docker_workshop_open_cmd.go @@ -14,12 +14,16 @@ import ( "github.com/docker/docker/client" "github.com/pkg/errors" "github.com/spf13/cobra" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) type DockerWorkshopOpenOptions struct { - Name string - Path string + Name string + Path string + WorkshopFile string + WorkshopVersion string + DataValuesFlags yttcmd.DataValuesFlags } func (o *DockerWorkshopOpenOptions) Run() error { @@ -44,7 +48,7 @@ func (o *DockerWorkshopOpenOptions) Run() error { var workshop *unstructured.Unstructured - if workshop, err = loadWorkshopDefinition(o.Name, path, "educates-cli"); err != nil { + if workshop, err = loadWorkshopDefinition(o.Name, path, "educates-cli", o.WorkshopFile, o.WorkshopVersion, o.DataValuesFlags); err != nil { return err } @@ -134,5 +138,57 @@ func (p *ProjectInfo) NewDockerWorkshopOpenCmd() *cobra.Command { "path to local workshop directory, definition file, or URL for workshop definition file", ) + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop being published", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + return c } diff --git a/client-programs/pkg/cmd/educates_cmd_group.go b/client-programs/pkg/cmd/educates_cmd_group.go index 7b35388c..c31ee558 100644 --- a/client-programs/pkg/cmd/educates_cmd_group.go +++ b/client-programs/pkg/cmd/educates_cmd_group.go @@ -40,9 +40,11 @@ func (p *ProjectInfo) NewEducatesCmdGroup() *cobra.Command { Commands: []*cobra.Command{ overrideCommandName(p.NewWorkshopNewCmd(), "new-workshop"), overrideCommandName(p.NewWorkshopPublishCmd(), "publish-workshop"), + overrideCommandName(p.NewWorkshopExportCmd(), "export-workshop"), overrideCommandName(p.NewClusterWorkshopDeployCmd(), "deploy-workshop"), overrideCommandName(p.NewClusterWorkshopListCmd(), "list-workshops"), overrideCommandName(p.NewClusterWorkshopRequestCmd(), "request-workshop"), + overrideCommandName(p.NewClusterWorkshopServeCmd(), "serve-workshop"), overrideCommandName(p.NewClusterWorkshopUpdateCmd(), "update-workshop"), overrideCommandName(p.NewClusterWorkshopDeleteCmd(), "delete-workshop"), overrideCommandName(p.NewClusterPortalOpenCmd(), "browse-workshops"), diff --git a/client-programs/pkg/cmd/tunnel_connect_cmd.go b/client-programs/pkg/cmd/tunnel_connect_cmd.go index 72c35e3f..551b8581 100644 --- a/client-programs/pkg/cmd/tunnel_connect_cmd.go +++ b/client-programs/pkg/cmd/tunnel_connect_cmd.go @@ -113,7 +113,7 @@ func (s *session) readRemote() { switch msgType { case websocket.BinaryMessage: if _, err = out.Write(buf); err != nil { - break + return } default: s.errChan <- fmt.Errorf("unexpected websocket frame type: %d", msgType) diff --git a/client-programs/pkg/cmd/workshop_cmd_group.go b/client-programs/pkg/cmd/workshop_cmd_group.go index bb41185f..f0f6bfa7 100644 --- a/client-programs/pkg/cmd/workshop_cmd_group.go +++ b/client-programs/pkg/cmd/workshop_cmd_group.go @@ -27,6 +27,7 @@ func (p *ProjectInfo) NewWorkshopCmdGroup() *cobra.Command { Commands: []*cobra.Command{ p.NewWorkshopNewCmd(), p.NewWorkshopPublishCmd(), + p.NewWorkshopExportCmd(), }, }, } diff --git a/client-programs/pkg/cmd/workshop_export_cmd.go b/client-programs/pkg/cmd/workshop_export_cmd.go new file mode 100644 index 00000000..535b1647 --- /dev/null +++ b/client-programs/pkg/cmd/workshop_export_cmd.go @@ -0,0 +1,186 @@ +// Copyright 2022-2023 The Educates Authors. + +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" + "gopkg.in/yaml.v2" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/kubectl/pkg/scheme" +) + +type FilesExportOptions struct { + Repository string + WorkshopFile string + WorkshopVersion string + DataValuesFlags yttcmd.DataValuesFlags +} + +func (o *FilesExportOptions) Run(args []string) error { + var err error + + var directory string + + if len(args) != 0 { + directory = filepath.Clean(args[0]) + } else { + directory = "." + } + + if directory, err = filepath.Abs(directory); err != nil { + return errors.Wrap(err, "couldn't convert workshop directory to absolute path") + } + + fileInfo, err := os.Stat(directory) + + if err != nil || !fileInfo.IsDir() { + return errors.New("workshop directory does not exist or path is not a directory") + } + + return o.Export(directory) +} + +func (o *FilesExportOptions) Export(directory string) error { + // If image name hasn't been supplied read workshop definition file and + // try to work out image name to Export workshop as. + + rootDirectory := directory + workshopFilePath := o.WorkshopFile + + if !filepath.IsAbs(workshopFilePath) { + workshopFilePath = filepath.Join(rootDirectory, workshopFilePath) + } + + workshopFileData, err := os.ReadFile(workshopFilePath) + + if err != nil { + return errors.Wrapf(err, "cannot open workshop definition %q", workshopFilePath) + } + + // Process the workshop YAML data for ytt templating and data variables. + + if workshopFileData, err = processWorkshopDefinition(workshopFileData, o.DataValuesFlags); err != nil { + return errors.Wrap(err, "unable to process workshop definition as template") + } + + workshopFileData = []byte(strings.ReplaceAll(string(workshopFileData), "$(image_repository)", o.Repository)) + workshopFileData = []byte(strings.ReplaceAll(string(workshopFileData), "$(workshop_version)", o.WorkshopVersion)) + + decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDecoder() + + workshop := &unstructured.Unstructured{} + + err = runtime.DecodeInto(decoder, workshopFileData, workshop) + + if err != nil { + return errors.Wrap(err, "couldn't parse workshop definition") + } + + if workshop.GetAPIVersion() != "training.educates.dev/v1beta1" || workshop.GetKind() != "Workshop" { + return errors.New("invalid type for workshop definition") + } + + // Insert workshop version property if not specified. + + _, found, _ := unstructured.NestedString(workshop.Object, "spec", "version") + + if !found && o.WorkshopVersion != "latest" { + unstructured.SetNestedField(workshop.Object, o.WorkshopVersion, "spec", "version") + } + + // Remove the publish section as will not be accurate after publising. + + unstructured.RemoveNestedField(workshop.Object, "spec", "publish") + + // Export modified workshop definition file. + + workshopFileData, err = yaml.Marshal(&workshop.Object) + + if err != nil { + return errors.Wrap(err, "couldn't convert workshop definition back to YAML") + } + + fmt.Print(string(workshopFileData)) + + return nil +} + +func (p *ProjectInfo) NewWorkshopExportCmd() *cobra.Command { + var o FilesExportOptions + + var c = &cobra.Command{ + Args: cobra.MaximumNArgs(1), + Use: "export [PATH]", + Short: "Export workshop resource definition", + RunE: func(cmd *cobra.Command, args []string) error { return o.Run(args) }, + } + + c.Flags().StringVar( + &o.Repository, + "image-repository", + "localhost:5001", + "the address of the image repository", + ) + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop being published", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + + return c +} diff --git a/client-programs/pkg/cmd/workshop_new_cmd.go b/client-programs/pkg/cmd/workshop_new_cmd.go index 4cf3f719..2a60791c 100644 --- a/client-programs/pkg/cmd/workshop_new_cmd.go +++ b/client-programs/pkg/cmd/workshop_new_cmd.go @@ -68,7 +68,7 @@ func (p *ProjectInfo) NewWorkshopNewCmd() *cobra.Command { &o.Template, "template", "t", - "basic", + "classic", "name of the workshop template to use", ) c.Flags().StringVarP( diff --git a/client-programs/pkg/cmd/workshop_publish_cmd.go b/client-programs/pkg/cmd/workshop_publish_cmd.go index ca942894..a1b738a3 100644 --- a/client-programs/pkg/cmd/workshop_publish_cmd.go +++ b/client-programs/pkg/cmd/workshop_publish_cmd.go @@ -3,15 +3,25 @@ package cmd import ( + "bytes" + "fmt" + "log" "os" "path/filepath" "strings" + "time" "github.com/cppforlife/go-cli-ui/ui" "github.com/pkg/errors" "github.com/spf13/cobra" imgpkgcmd "github.com/vmware-tanzu/carvel-imgpkg/pkg/imgpkg/cmd" "github.com/vmware-tanzu/carvel-kapp/pkg/kapp/cmd" + vendirsync "github.com/vmware-tanzu/carvel-vendir/pkg/vendir/cmd" + yttcmd "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template" + yttcmdui "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/ui" + "github.com/vmware-tanzu/carvel-ytt/pkg/files" + "github.com/vmware-tanzu/carvel-ytt/pkg/yamlmeta" + "gopkg.in/yaml.v2" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -21,120 +31,108 @@ import ( ) type FilesPublishOptions struct { - Image string - Repository string + Image string + Repository string + WorkshopFile string + ExportWorkshop string + WorkshopVersion string + RegistryFlags imgpkgcmd.RegistryFlags + DataValuesFlags yttcmd.DataValuesFlags } -func (p *ProjectInfo) NewWorkshopPublishCmd() *cobra.Command { - var o FilesPublishOptions - - var c = &cobra.Command{ - Args: cobra.MaximumNArgs(1), - Use: "publish [PATH]", - Short: "Publish workshop files to repository", - RunE: func(_ *cobra.Command, args []string) error { - var err error +func (o *FilesPublishOptions) Run(args []string) error { + var err error - var directory string + var directory string - if len(args) != 0 { - directory = filepath.Clean(args[0]) - } else { - directory = "." - } - - if directory, err = filepath.Abs(directory); err != nil { - return errors.Wrap(err, "couldn't convert workshop directory to absolute path") - } + if len(args) != 0 { + directory = filepath.Clean(args[0]) + } else { + directory = "." + } - fileInfo, err := os.Stat(directory) + if directory, err = filepath.Abs(directory); err != nil { + return errors.Wrap(err, "couldn't convert workshop directory to absolute path") + } - if err != nil || !fileInfo.IsDir() { - return errors.New("workshop directory does not exist or path is not a directory") - } + fileInfo, err := os.Stat(directory) - if o.Repository == "localhost:5001" { - err = registry.DeployRegistry() + if err != nil || !fileInfo.IsDir() { + return errors.New("workshop directory does not exist or path is not a directory") + } - if err != nil { - return errors.Wrap(err, "failed to deploy registry") - } - } + if o.Repository == "localhost:5001" { + err = registry.DeployRegistry() - return publishWorkshopDirectory(directory, o.Image, o.Repository) - }, + if err != nil { + return errors.Wrap(err, "failed to deploy registry") + } } - c.Flags().StringVar( - &o.Image, - "image", - "", - "name of the workshop files image artifact", - ) - c.Flags().StringVar( - &o.Repository, - "repository", - "localhost:5001", - "the address of the image repository", - ) - - return c + return o.Publish(directory) } -func publishWorkshopDirectory(directory string, image string, repository string) error { +func (o *FilesPublishOptions) Publish(directory string) error { // If image name hasn't been supplied read workshop definition file and // try to work out image name to publish workshop as. rootDirectory := directory + workshopFilePath := o.WorkshopFile - if image == "" { - workshopFilePath := filepath.Join(directory, "resources", "workshop.yaml") + workingDirectory, err := os.Getwd() - workshopFileData, err := os.ReadFile(workshopFilePath) + if err != nil { + return errors.Wrap(err, "cannot determine current working directory") + } - if err != nil { - return errors.Wrapf(err, "cannot open workshop definition %q", workshopFilePath) - } + includePaths := []string{directory} + excludePaths := []string{".git"} - decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDecoder() + if !filepath.IsAbs(workshopFilePath) { + workshopFilePath = filepath.Join(rootDirectory, workshopFilePath) + } - workshop := &unstructured.Unstructured{} + workshopFileData, err := os.ReadFile(workshopFilePath) - err = runtime.DecodeInto(decoder, workshopFileData, workshop) + if err != nil { + return errors.Wrapf(err, "cannot open workshop definition %q", workshopFilePath) + } - if err != nil { - return errors.Wrap(err, "couldn't parse workshop definition") - } + // Process the workshop YAML data for ytt templating and data variables. - fileArtifacts, found, err := unstructured.NestedSlice(workshop.Object, "spec", "workshop", "files") + if workshopFileData, err = processWorkshopDefinition(workshopFileData, o.DataValuesFlags); err != nil { + return errors.Wrap(err, "unable to process workshop definition as template") + } - if err != nil || !found { - return errors.Errorf("cannot find image specification in %q", workshopFilePath) - } + workshopFileData = []byte(strings.ReplaceAll(string(workshopFileData), "$(image_repository)", o.Repository)) + workshopFileData = []byte(strings.ReplaceAll(string(workshopFileData), "$(workshop_version)", o.WorkshopVersion)) - for _, artifactEntry := range fileArtifacts { - if imageDetails, ok := artifactEntry.(map[string]interface{})["image"]; ok { - if unpackPath, ok := artifactEntry.(map[string]interface{})["path"]; !ok || (ok && (unpackPath == nil || unpackPath.(string) == "" || unpackPath.(string) == ".")) { - if imageUrl, ok := imageDetails.(map[string]interface{})["url"]; ok { - image = strings.ReplaceAll(imageUrl.(string), "$(image_repository)", repository) - - if newRootPath, ok := artifactEntry.(map[string]interface{})["newRootPath"]; ok { - suffix := "/" + newRootPath.(string) - if strings.HasSuffix(directory, suffix) { - rootDirectory = strings.TrimSuffix(directory, suffix) - } - } - } - } - } - } + decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDecoder() + + workshop := &unstructured.Unstructured{} + + err = runtime.DecodeInto(decoder, workshopFileData, workshop) + + if err != nil { + return errors.Wrap(err, "couldn't parse workshop definition") + } + + if workshop.GetAPIVersion() != "training.educates.dev/v1beta1" || workshop.GetKind() != "Workshop" { + return errors.New("invalid type for workshop definition") } + image := o.Image + if image == "" { - return errors.New("cannot determine name of image to publish as") + image, _, _ = unstructured.NestedString(workshop.Object, "spec", "publish", "image") } - // Now publish workshop directory contents as OCI image artifact. + if image == "" { + return errors.Errorf("cannot find image name for publishing workshop %q", workshopFilePath) + } + + // Extract vendir snippet describing subset of files to package up as the + // workshop image. confUI := ui.NewConfUI(ui.NewNoopLogger()) @@ -148,17 +146,321 @@ func publishWorkshopDirectory(directory string, image string, repository string) defer confUI.Flush() - var pushOptions = imgpkgcmd.NewPushOptions(confUI) + if fileArtifacts, found, _ := unstructured.NestedSlice(workshop.Object, "spec", "publish", "files"); found && len(fileArtifacts) != 0 { + tempDir, err := os.MkdirTemp("", "educates-imgpkg") + + if err != nil { + return errors.Wrapf(err, "unable to create temporary working directory") + } + + defer os.RemoveAll(tempDir) + + for _, artifactEntry := range fileArtifacts { + vendirConfig := map[string]interface{}{ + "apiVersion": "vendir.k14s.io/v1alpha1", + "kind": "Config", + "directories": []interface{}{}, + } + + dir := filepath.Join(tempDir, "files") + + if filePath, found := artifactEntry.(map[string]interface{})["path"].(string); found { + dir = filepath.Join(tempDir, "files", filepath.Clean(filePath)) + } + + if directoryConfig, found := artifactEntry.(map[string]interface{})["directory"]; found { + if directoryPath, found := directoryConfig.(map[string]interface{})["path"].(string); found { + if !filepath.IsAbs(directoryPath) { + directoryConfig.(map[string]interface{})["path"] = filepath.Join(directory, directoryPath) + } + } + } + + artifactEntry.(map[string]interface{})["path"] = "." + + directoryConfig := map[string]interface{}{ + "path": dir, + "contents": []interface{}{artifactEntry}, + } + + vendirConfig["directories"] = append(vendirConfig["directories"].([]interface{}), directoryConfig) + + yamlData, err := yaml.Marshal(&vendirConfig) + + if err != nil { + return errors.Wrap(err, "unable to generate vendir config") + } + + vendirConfigFile, err := os.Create(filepath.Join(tempDir, "vendir.yml")) + + if err != nil { + return errors.Wrap(err, "unable to create vendir config file") + } + + defer vendirConfigFile.Close() + + _, err = vendirConfigFile.Write(yamlData) + + if err != nil { + return errors.Wrap(err, "unable to write vendir config file") + } + + syncOptions := vendirsync.NewSyncOptions(confUI) + + syncOptions.Directories = nil + syncOptions.Files = []string{filepath.Join(tempDir, "vendir.yml")} + + // Note that Chdir here actually changes the process working directory. + + syncOptions.LockFile = filepath.Join(tempDir, "lock-file") + syncOptions.Locked = false + syncOptions.Chdir = tempDir + syncOptions.AllowAllSymlinkDestinations = false + + if err = syncOptions.Run(); err != nil { + fmt.Println(string(yamlData)) + + return errors.Wrap(err, "failed to prepare image files for publishing") + } + } + + // Restore working directory as was changed. + + os.Chdir((workingDirectory)) + + rootDirectory = filepath.Join(tempDir, "files") + includePaths = []string{rootDirectory} + } + + // Now publish workshop directory contents as OCI image artifact. + + pushOptions := imgpkgcmd.NewPushOptions(confUI) pushOptions.ImageFlags.Image = image - pushOptions.FileFlags.Files = append(pushOptions.FileFlags.Files, rootDirectory) - pushOptions.FileFlags.ExcludedFilePaths = append(pushOptions.FileFlags.ExcludedFilePaths, ".git") + pushOptions.FileFlags.Files = append(pushOptions.FileFlags.Files, includePaths...) + pushOptions.FileFlags.ExcludedFilePaths = append(pushOptions.FileFlags.ExcludedFilePaths, excludePaths...) + + pushOptions.RegistryFlags = o.RegistryFlags - err := pushOptions.Run() + err = pushOptions.Run() if err != nil { return errors.Wrap(err, "unable to push image artifact for workshop") } + // Export modified workshop definition file. + + exportWorkshop := o.ExportWorkshop + + if exportWorkshop != "" { + // Insert workshop version property if not specified. + + _, found, _ := unstructured.NestedString(workshop.Object, "spec", "version") + + if !found && o.WorkshopVersion != "latest" { + unstructured.SetNestedField(workshop.Object, o.WorkshopVersion, "spec", "version") + } + + // Remove the publish section as will not be accurate after publising. + + unstructured.RemoveNestedField(workshop.Object, "spec", "publish") + + workshopFileData, err = yaml.Marshal(&workshop.Object) + + if err != nil { + return errors.Wrap(err, "couldn't convert workshop definition back to YAML") + } + + if !filepath.IsAbs(exportWorkshop) { + exportWorkshop = filepath.Join(workingDirectory, exportWorkshop) + } + + exportWorkshopFile, err := os.Create(exportWorkshop) + + if err != nil { + return errors.Wrap(err, "unable to create exported workshop definition file") + } + + defer exportWorkshopFile.Close() + + _, err = exportWorkshopFile.Write(workshopFileData) + + if err != nil { + return errors.Wrap(err, "unable to write exported workshop definition file") + } + } + return nil } + +func (p *ProjectInfo) NewWorkshopPublishCmd() *cobra.Command { + var o FilesPublishOptions + + var c = &cobra.Command{ + Args: cobra.MaximumNArgs(1), + Use: "publish [PATH]", + Short: "Publish workshop files to repository", + RunE: func(cmd *cobra.Command, args []string) error { return o.Run(args) }, + } + + c.Flags().StringVar( + &o.Image, + "image", + "", + "name of the workshop files image artifact", + ) + c.Flags().StringVar( + &o.Repository, + "image-repository", + "localhost:5001", + "the address of the image repository", + ) + c.Flags().StringVar( + &o.WorkshopFile, + "workshop-file", + "resources/workshop.yaml", + "location of the workshop definition file", + ) + c.Flags().StringVar( + &o.ExportWorkshop, + "export-workshop", + "", + "location to save modified workshop file", + ) + + c.Flags().StringVar( + &o.WorkshopVersion, + "workshop-version", + "latest", + "version of the workshop being published", + ) + + c.Flags().StringSliceVar( + &o.RegistryFlags.CACertPaths, + "registry-ca-cert-path", + nil, + "Add CA certificates for registry API", + ) + c.Flags().BoolVar( + &o.RegistryFlags.VerifyCerts, + "registry-verify-certs", + true, + "Set whether to verify server's certificate chain and host name", + ) + c.Flags().BoolVar( + &o.RegistryFlags.Insecure, + "registry-insecure", + false, + "Allow the use of http when interacting with registries", + ) + + c.Flags().StringVar( + &o.RegistryFlags.Username, + "registry-username", + "", + "Set username for registry authentication", + ) + c.Flags().StringVar( + &o.RegistryFlags.Password, + "registry-password", + "", + "Set password for registry authentication", + ) + c.Flags().StringVar( + &o.RegistryFlags.Token, + "registry-token", + "", + "Set token for registry authentication", + ) + c.Flags().BoolVar( + &o.RegistryFlags.Anon, + "registry-anon", + false, + "Set anonymous for registry authentication", + ) + + c.Flags().DurationVar( + &o.RegistryFlags.ResponseHeaderTimeout, + "registry-response-header-timeout", + 30*time.Second, + "Maximum time to allow a request to wait for a server's response headers from the registry (ms|s|m|h)", + ) + c.Flags().IntVar( + &o.RegistryFlags.RetryCount, + "registry-retry-count", + 5, + "Set the number of times imgpkg retries to send requests to the registry in case of an error", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromStrings, + "data-values-env", + nil, + "Extract data values (as strings) from prefixed env vars (format: PREFIX for PREFIX_all__key1=str) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.EnvFromYAML, + "data-values-env-yaml", + nil, + "Extract data values (parsed as YAML) from prefixed env vars (format: PREFIX for PREFIX_all__key1=true) (can be specified multiple times)", + ) + + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromStrings, + "data-value", + nil, + "Set specific data value to given value, as string (format: all.key1.subkey=123) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromYAML, + "data-value-yaml", + nil, + "Set specific data value to given value, parsed as YAML (format: all.key1.subkey=true) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.KVsFromFiles, + "data-value-file", + nil, + "Set specific data value to contents of a file (format: [@lib1:]all.key1.subkey={file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + c.Flags().StringArrayVar( + &o.DataValuesFlags.FromFiles, + "data-values-file", + nil, + "Set multiple data values via plain YAML files (format: [@lib1:]{file path, HTTP URL, or '-' (i.e. stdin)}) (can be specified multiple times)", + ) + + return c +} + +func processWorkshopDefinition(yamlData []byte, dataValueFlags yttcmd.DataValuesFlags) ([]byte, error) { + templatingOptions := yttcmd.NewOptions() + + templatingOptions.IgnoreUnknownComments = true + + templatingOptions.DataValuesFlags = dataValueFlags + + var filesToProcess []*files.File + + mainInputFile := files.MustNewFileFromSource(files.NewBytesSource("workshop.yaml", yamlData)) + + filesToProcess = append(filesToProcess, mainInputFile) + + logUI := yttcmdui.NewCustomWriterTTY(false, log.Writer(), log.Writer()) + + output := templatingOptions.RunWithFiles(yttcmd.Input{Files: filesToProcess}, logUI) + + if output.Err != nil { + return []byte{}, fmt.Errorf("execution of ytt failed: %s", output.Err) + } + + if len(output.DocSet.Items) == 0 { + return []byte{}, nil + } + + var buf bytes.Buffer + + yamlmeta.NewYAMLPrinter(&buf).Print(output.DocSet.Items[0]) + + return buf.Bytes(), nil +} diff --git a/client-programs/pkg/config/installationconfig.go b/client-programs/pkg/config/installationconfig.go index 0a38a853..5a038788 100644 --- a/client-programs/pkg/config/installationconfig.go +++ b/client-programs/pkg/config/installationconfig.go @@ -80,6 +80,10 @@ type ClusterIngressConfig struct { CANodeInjector CANodeInjectorConfig `yaml:"caNodeInjector,omitempty"` } +type SessionCookiesConfig struct { + Domain string `yaml:"domain,omitempty"` +} + type ClusterStorageConfig struct { Class string `yaml:"class,omitempty"` User int `yaml:"user,omitempty"` @@ -96,7 +100,7 @@ type PullSecretRefConfig struct { } type ClusterSecretsConfig struct { - PullSecretRefs []PullSecretRefConfig + PullSecretRefs []PullSecretRefConfig `yaml:"pullSecretRefs"` } type UserCredentialsConfig struct { @@ -127,10 +131,6 @@ type ImageVersionConfig struct { Image string `yaml:"image"` } -type ImageVersionsConfig struct { - ImageVersions []ImageVersionConfig -} - type ProxyCacheConfig struct { RemoteURL string `yaml:"remoteURL"` Username string `yaml:"username,omitempty"` @@ -191,7 +191,9 @@ type WebsiteStylingConfig struct { TrainingPortal WebsiteStyleOverridesConfig `yaml:"trainingPortal,omitempty"` WorkshopStarted WebsiteHTMLSnippetConfig `yaml:"workshopStarted,omitempty"` WorkshopFinished WebsiteHTMLSnippetConfig `yaml:"workshopFinished,omitempty"` + DefaultTheme string `yaml:"defaultTheme,omitempty"` ThemeDataRefs []ThemeDataRefConfig `yaml:"themeDataRefs,omitempty"` + FrameAncestors []string `yaml:"frameAncestors,omitempty"` } type ClusterEssentialsConfig struct { @@ -204,12 +206,13 @@ type TrainingPlatformConfig struct { ClusterSecurity ClusterSecurityConfig `yaml:"clusterSecurity,omitempty"` ClusterRuntime ClusterRuntimeConfig `yaml:"clusterRuntime,omitempty"` ClusterIngress ClusterIngressConfig `yaml:"clusterIngress,omitempty"` + SessionCookies SessionCookiesConfig `yaml:"sessionCookies,omitempty"` ClusterStorage ClusterStorageConfig `yaml:"clusterStorage,omitempty"` ClusterSecrets ClusterSecretsConfig `yaml:"clusterSecrets,omitempty"` TrainingPortal TrainingPortalConfig `yaml:"trainingPortal,omitempty"` WorkshopSecurity WorkshopSecurityConfig `yaml:"workshopSecurity,omitempty"` ImageRegistry ImageRegistryConfig `yaml:"imageRegistry,omitempty"` - ImageVersions ImageVersionsConfig `yaml:"imageVersions,omitempty"` + ImageVersions []ImageVersionConfig `yaml:"imageVersions,omitempty"` DockerDaemon DockerDaemonConfig `yaml:"dockerDaemon,omitempty"` ClusterNetwork ClusterNetworkConfig `yaml:"clusterNetwork,omitempty"` WorkshopAnalytics WorkshopAnalyticsConfig `yaml:"workshopAnalytics,omitempty"` @@ -224,12 +227,13 @@ type InstallationConfig struct { ClusterSecurity ClusterSecurityConfig `yaml:"clusterSecurity,omitempty"` ClusterRuntime ClusterRuntimeConfig `yaml:"clusterRuntime,omitempty"` ClusterIngress ClusterIngressConfig `yaml:"clusterIngress,omitempty"` + SessionCookies SessionCookiesConfig `yaml:"sessionCookies,omitempty"` ClusterStorage ClusterStorageConfig `yaml:"clusterStorage,omitempty"` ClusterSecrets ClusterSecretsConfig `yaml:"clusterSecrets,omitempty"` TrainingPortal TrainingPortalConfig `yaml:"trainingPortal,omitempty"` WorkshopSecurity WorkshopSecurityConfig `yaml:"workshopSecurity,omitempty"` ImageRegistry ImageRegistryConfig `yaml:"imageRegistry,omitempty"` - ImageVersions ImageVersionsConfig `yaml:"imageVersions,omitempty"` + ImageVersions []ImageVersionConfig `yaml:"imageVersions,omitempty"` DockerDaemon DockerDaemonConfig `yaml:"dockerDaemon,omitempty"` ClusterNetwork ClusterNetworkConfig `yaml:"clusterNetwork,omitempty"` WorkshopAnalytics WorkshopAnalyticsConfig `yaml:"workshopAnalytics,omitempty"` diff --git a/client-programs/pkg/registry/registry.go b/client-programs/pkg/registry/registry.go index 92532f59..0751f889 100644 --- a/client-programs/pkg/registry/registry.go +++ b/client-programs/pkg/registry/registry.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "os" - "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -19,6 +18,7 @@ import ( v1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" ) @@ -147,13 +147,13 @@ func DeleteRegistry() error { return nil } - // timeout := 30 + timeout := 30 - // err = cli.ContainerStop(ctx, "educates-registry", container.StopOptions{Timeout: &timeout}) + err = cli.ContainerStop(ctx, "educates-registry", container.StopOptions{Timeout: &timeout}) - timeout := time.Duration(30) * time.Second + // timeout := time.Duration(30) * time.Second - err = cli.ContainerStop(ctx, "educates-registry", &timeout) + // err = cli.ContainerStop(ctx, "educates-registry", &timeout) if err != nil { return errors.Wrap(err, "unable to stop registry container") @@ -185,7 +185,8 @@ func UpdateRegistryService(k8sclient *kubernetes.Clientset) error { Type: apiv1.ServiceTypeClusterIP, Ports: []apiv1.ServicePort{ { - Port: 5001, + Port: 80, + TargetPort: intstr.FromInt(5001), }, }, }, diff --git a/client-programs/pkg/renderer/hugo.go b/client-programs/pkg/renderer/hugo.go new file mode 100644 index 00000000..3822a894 --- /dev/null +++ b/client-programs/pkg/renderer/hugo.go @@ -0,0 +1,609 @@ +// Copyright 2022-2023 The Educates Authors. + +package renderer + +import ( + "archive/tar" + "context" + "embed" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/exec" + "os/signal" + "path" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/pkg/errors" + "github.com/vmware-tanzu-labs/educates-training-platform/client-programs/pkg/cluster" + "gopkg.in/yaml.v2" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +//go:embed all:files/* +var hugoFiles embed.FS + +func copyFiles(fs embed.FS, src string, dst string) error { + files, err := hugoFiles.ReadDir(src) + + if err != nil { + return errors.Wrapf(err, "unable to open files directory %q", src) + } + + for _, file := range files { + srcFile := path.Join(src, file.Name()) + dstFile := path.Join(dst, file.Name()) + + if file.IsDir() { + if err = os.MkdirAll(dstFile, 0775); err != nil { + return errors.Wrapf(err, "unable to create workshop directory %q", dstFile) + } + + if err = copyFiles(fs, srcFile, dstFile); err != nil { + return err + } + } else { + input, err := fs.ReadFile(srcFile) + + if err != nil { + return errors.Wrapf(err, "unable to open source file %q", srcFile) + } + + err = ioutil.WriteFile(dstFile, input, 0644) + + if err != nil { + return errors.Wrapf(err, "unable to create source file %q", dstFile) + } + } + } + + return nil +} + +type WorkshopParamsConfig struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + Aliases []string `yaml:"aliases,omitempty"` +} + +type WorkshopPathConfig struct { + Title string `yaml:"title,omitempty"` + Description string `yaml:"description,omitempty"` + Params []WorkshopParamsConfig `yaml:"params,omitempty"` + Steps []string `yaml:"steps,omitempty"` +} + +type WorkshopModuleConfig struct { + Title string `yaml:"title"` + Path string `yaml:"path"` + PrevPage string `yaml:"prev_page"` + NextPage string `yaml:"next_page"` + Step int `yaml:"step"` +} + +type WorkshopPathwaysConfig struct { + Default string `yaml:"default,omitempty"` + Paths map[string]WorkshopPathConfig `yaml:"paths,omitempty"` + Modules map[string]WorkshopModuleConfig `yaml:"modules,omitempty"` +} + +type WorkshopConfig struct { + Pathways WorkshopPathwaysConfig `yaml:"pathways,omitempty"` + Params []WorkshopParamsConfig `yaml:"params,omitempty"` +} + +var workshopSessionResource = schema.GroupVersionResource{Group: "training.educates.dev", Version: "v1beta1", Resource: "workshopsessions"} + +func fetchWorkshopSessionAndValidate(kubeconfig string, workshop string, portal string, session string) (string, string, error) { + // Returns session URL, config password and error. + + var err error + + clusterConfig := cluster.NewClusterConfig(kubeconfig) + + dynamicClient, err := clusterConfig.GetDynamicClient() + + if err != nil { + return "", "", errors.Wrapf(err, "unable to create Kubernetes client") + } + + workshopSessionClient := dynamicClient.Resource(workshopSessionResource) + + workshopSession, err := workshopSessionClient.Get(context.TODO(), session, metav1.GetOptions{}) + + if k8serrors.IsNotFound(err) { + return "", "", errors.New("no workshop session can be found") + } + + linkedWorkshop, _, _ := unstructured.NestedString(workshopSession.Object, "spec", "workshop", "name") + + if linkedWorkshop != workshop { + return "", "", errors.New("workshop session not linked to target workshop") + } + + linkedPortal, _, _ := unstructured.NestedString(workshopSession.Object, "spec", "portal", "name") + + if linkedPortal != portal { + return "", "", errors.New("workshop session not linked to target portal") + } + + password, _, _ := unstructured.NestedString(workshopSession.Object, "spec", "session", "config", "password") + sessionURL, _, _ := unstructured.NestedString(workshopSession.Object, "status", "educates", "url") + + if password == "" { + return "", "", errors.New("cannot determine config password for session") + } + + if sessionURL == "" { + return "", "", errors.New("cannot determine url for accessing workshop session") + } + + return sessionURL, password, nil +} + +func fetchSessionVariables(sessionURL string, password string) (map[string]string, error) { + var err error + + params := make(map[string]string) + + url := fmt.Sprintf("%s/config/variables", sessionURL) + + req, err := http.NewRequest("GET", url, nil) + + if err != nil { + return params, errors.Wrapf(err, "cannot construct request to query workshop session config") + } + + q := req.URL.Query() + q.Add("token", password) + req.URL.RawQuery = q.Encode() + + res, err := http.DefaultClient.Do(req) + + if err != nil { + return params, errors.Wrapf(err, "cannot query workshop session config") + } + + if res.StatusCode != 200 { + return params, errors.New("unexpected failure querying workshop session config") + } + + resBody, err := ioutil.ReadAll(res.Body) + + if err != nil { + return params, errors.Wrapf(err, "failed to read workshop session config") + } + + err = json.Unmarshal(resBody, ¶ms) + + if err != nil { + return params, errors.Wrapf(err, "unable to unpack workshop session parameters") + } + + return params, nil +} + +func generateHugoConfiguration(workshopDir string, target string, params map[string]string, sessionURL string) error { + var err error + + // Read user workshop config with details of any pathways. + + workshopConfigPath := filepath.Join(workshopDir, "config.yaml") + + workshopConfigData, err := os.ReadFile(workshopConfigPath) + + activeModules := map[string]*WorkshopModuleConfig{} + + if err == nil { + // Assume file doesn't exist if had an error and skip it. + + workshopConfig := WorkshopConfig{} + + err = yaml.Unmarshal(workshopConfigData, &workshopConfig) + + if err != nil { + return errors.Wrapf(err, "unable to unpack workshop config") + } + + // Use the pathway name calculated from the workshop session and if + // not defined fallback to the using default pathway name if specified. + + pathwayName := params["pathway_name"] + + if pathwayName == "" { + if len(workshopConfig.Pathways.Paths) != 0 { + pathwayName = workshopConfig.Pathways.Default + } + } + + pathway, pathwayExists := workshopConfig.Pathways.Paths[pathwayName] + + if pathwayName != "" && pathwayExists && len(pathway.Steps) != 0 { + modules := workshopConfig.Pathways.Modules + + firstPage := "" + prevPage := "" + + for index, step := range pathway.Steps { + if firstPage == "" { + firstPage = step + } + + module, moduleExists := modules[step] + + if !moduleExists { + module = WorkshopModuleConfig{} + } + + module.Path = step + + if prevPage != "" { + module.PrevPage = prevPage + activeModules[prevPage].NextPage = step + } else { + module.PrevPage = "" + } + + module.NextPage = "" + module.Step = index + 1 + + prevPage = step + + activeModules[step] = &module + } + + params["__first_page__"] = firstPage + } + } + + type HugoConfig struct { + BaseURL string `yaml:"baseURL"` + Params map[string]interface{} `yaml:"params"` + } + + config := HugoConfig{Params: make(map[string]interface{})} + + config.BaseURL = fmt.Sprintf("%s/workshop/content/", sessionURL) + + for paramName, paramValue := range params { + config.Params[paramName] = paramValue + } + + config.Params["__modules__"] = activeModules + + configData, err := yaml.Marshal(config) + + if err != nil { + return errors.Wrapf(err, "unable to marshal hugo configuration") + } + + if err != nil { + return errors.Wrapf(err, "unable to create hugo files directory") + } + + targetFile := filepath.Join(target, "hugo.yaml") + workingFile := filepath.Join(target, "hugo.yaml.tmp") + + configFile, err := os.Create(workingFile) + + if err != nil { + return errors.Wrapf(err, "unable to create working hugo config file") + } + + _, err = configFile.Write(configData) + + if err != nil { + return errors.Wrapf(err, "unable to write working hugo config file") + } + + configFile.Close() + + err = os.Rename(workingFile, targetFile) + + if err != nil { + return errors.Wrapf(err, "unable to update hugo config file") + } + + return nil +} + +func startHugoServer(workshopDir string, tempDir string, port int, sessionURL string) error { + // Run this in a go routine. + + wsPort := 80 + + if strings.HasPrefix(sessionURL, "https://") { + wsPort = 443 + } + + commandArgs := []string{ + "server", + "--source", workshopDir, + "--port", strconv.Itoa(port), + "--disableFastRender", + "--liveReloadPort", fmt.Sprintf("%d", wsPort), + "--config", filepath.Join(tempDir, "hugo.yaml"), + "--themesDir", filepath.Join(tempDir, "themes"), + "--theme", "educates", + "--watch", + } + + commandPath, err := exec.LookPath("hugo") + + if err != nil { + return errors.Wrapf(err, "unable to find hugo program") + } + + command := exec.Command(commandPath, commandArgs...) + + stdout, err := command.StdoutPipe() + command.Stderr = command.Stdout + + if err != nil { + return errors.Wrapf(err, "unable to create command output pipe") + } + + if err = command.Start(); err != nil { + return errors.Wrapf(err, "failed to execute hugo program") + } + + for { + tmp := make([]byte, 1024) + _, err := stdout.Read(tmp) + fmt.Print(string(tmp)) + if err != nil { + break + } + } + + return nil +} + +func populateTemporaryDirectory() (string, error) { + tempDir, err := ioutil.TempDir("", "educates") + + if err != nil { + return "", errors.Wrapf(err, "unable to create hugo files directory") + } + + err = copyFiles(hugoFiles, "files", tempDir) + + if err != nil { + return "", errors.Wrapf(err, "failed to copy hugo files") + } + + return tempDir, nil +} + +type ServerCleanupFunc func() + +func RunHugoServer(workshopRoot string, kubeconfig string, workshop string, portal string, localHost string, localPort int, hugoPort int, token string, files bool, cleanupFunc ServerCleanupFunc) error { + var err error + var tempDir string + + workshopDir := filepath.Join(workshopRoot, "workshop") + + // First create directory to hold unpacked files for Hugo to use. + + if tempDir, err = populateTemporaryDirectory(); err != nil { + return err + } + + defer os.RemoveAll(tempDir) + + // Also catch signals so we can try and cleanup temporary directory. + + c := make(chan os.Signal) + + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + fmt.Println("Cleaning up...") + + os.RemoveAll(tempDir) + + if cleanupFunc != nil { + cleanupFunc() + } + + os.Exit(1) + }() + + // Now need to create a mini HTTP server to handle requests. + + var serverDetailsLock sync.Mutex + + var hugoStarted bool = false + var lastSessionName = "" + + proxyHandler := func(w http.ResponseWriter, r *http.Request) { + // If an access token is provided validate it. + + if token != "" { + accessToken := r.Header.Get("X-Access-Token") + + if accessToken != token { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("403 - Invalid access token")) + + return + } + } + + // Request must provide session name via header. + + sessionName := r.Header.Get("X-Session-Name") + + if sessionName == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("400 - Session name required")) + + return + } + + // If session name not the same as last seen, regenerate files. + + serverDetailsLock.Lock() + + if sessionName != lastSessionName { + // First validate that can access workshop session. + + sessionURL, password, err := fetchWorkshopSessionAndValidate(kubeconfig, workshop, portal, sessionName) + + if err != nil { + fmt.Println("Error validating workshop session:", err) + + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("500 - Unable to validate workshop session")) + + serverDetailsLock.Unlock() + + return + } + + // Then fetch back the session variables for the workshop session. + + params, err := fetchSessionVariables(sessionURL, password) + + if err != nil { + fmt.Println("Error fetching session variables:", err) + + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("500 - Unable to fetch workshop session config")) + + serverDetailsLock.Unlock() + + return + } + + // Generate (or regenerate) the Hugo configuration. + + err = generateHugoConfiguration(workshopDir, tempDir, params, sessionURL) + + if err != nil { + fmt.Println("Unable to generate Hugo configuration:", err) + + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("500 - Unable to generate server configuration")) + + serverDetailsLock.Unlock() + + return + } + + // If Hugo server is not already running it, start it. Add short + // delays to give the Hugo server time to start or reload. + + if !hugoStarted { + fmt.Println("Starting Hugo server") + + go startHugoServer(workshopDir, tempDir, hugoPort, sessionURL) + + time.Sleep(4 * time.Second) + + hugoStarted = true + } else { + time.Sleep(2 * time.Second) + } + + // Update last seen session name. + + lastSessionName = sessionName + } + + serverDetailsLock.Unlock() + + hugoServerURL := fmt.Sprintf("http://localhost:%d", hugoPort) + target, _ := url.Parse(hugoServerURL) + + proxy := httputil.NewSingleHostReverseProxy(target) + + proxyPass := func(res http.ResponseWriter, req *http.Request) { + proxy.ServeHTTP(w, req) + } + + proxyPass(w, r) + } + + http.HandleFunc("/workshop/content/", proxyHandler) + + filesHandler := func(w http.ResponseWriter, r *http.Request) { + if token != "" { + accessToken := r.URL.Query().Get("token") + + if accessToken != token { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("403 - Invalid access token")) + + return + } + } + + w.Header().Set("Content-Type", "application/x-tar") + + tw := tar.NewWriter(w) + + filepath.Walk(workshopRoot, func(file string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + header, err := tar.FileInfoHeader(fi, file) + if err != nil { + return err + } + + header.Name, err = filepath.Rel(workshopRoot, filepath.ToSlash(file)) + + if err != nil { + return err + } + + if header.Name == ".git" || strings.HasPrefix(header.Name, ".git/") { + return nil + } + + if err := tw.WriteHeader(header); err != nil { + return err + } + + if !fi.IsDir() { + data, err := os.Open(file) + if err != nil { + return err + } + if _, err := io.Copy(tw, data); err != nil { + return err + } + } + + return nil + }) + } + + if files { + http.HandleFunc("/workshop/files.tar", filesHandler) + } + + portString := fmt.Sprintf("%s:%d", localHost, localPort) + + fmt.Println("Proxy listening on:", portString) + + log.Fatal(http.ListenAndServe(portString, nil)) + + return nil +} diff --git a/client-programs/pkg/resolver/resolver.go b/client-programs/pkg/resolver/resolver.go index 70ff3d4f..8db89e06 100644 --- a/client-programs/pkg/resolver/resolver.go +++ b/client-programs/pkg/resolver/resolver.go @@ -10,7 +10,6 @@ import ( "io" "os" "path" - "time" "github.com/adrg/xdg" "github.com/docker/docker/api/types" @@ -179,13 +178,13 @@ func DeleteResolver() error { return nil } - // timeout := 30 + timeout := 30 - // err = cli.ContainerStop(ctx, "educates-resolver", container.StopOptions{Timeout: &timeout}) + err = cli.ContainerStop(ctx, "educates-resolver", container.StopOptions{Timeout: &timeout}) - timeout := time.Duration(30) * time.Second + // timeout := time.Duration(30) * time.Second - err = cli.ContainerStop(ctx, "educates-resolver", &timeout) + // err = cli.ContainerStop(ctx, "educates-resolver", &timeout) if err != nil { return errors.Wrap(err, "unable to stop DNS resolver container") diff --git a/client-programs/pkg/templates/files/basic/README.md b/client-programs/pkg/templates/files/classic/README.md similarity index 100% rename from client-programs/pkg/templates/files/basic/README.md rename to client-programs/pkg/templates/files/classic/README.md diff --git a/client-programs/pkg/templates/files/basic/resources/workshop.yaml b/client-programs/pkg/templates/files/classic/resources/workshop.yaml similarity index 81% rename from client-programs/pkg/templates/files/basic/resources/workshop.yaml rename to client-programs/pkg/templates/files/classic/resources/workshop.yaml index 3cc762ea..5ec268c6 100644 --- a/client-programs/pkg/templates/files/basic/resources/workshop.yaml +++ b/client-programs/pkg/templates/files/classic/resources/workshop.yaml @@ -5,13 +5,15 @@ metadata: spec: title: "{{ or .WorkshopTitle "Workshop" }}" description: "{{ or .WorkshopDescription "Workshop description." }}" + publish: + image: "$(image_repository)/{{ .WorkshopName }}-files:$(workshop_version)" workshop: {{- if .WorkshopImage }} image: "{{ .WorkshopImage }}" {{- end }} files: - image: - url: "$(image_repository)/{{ .WorkshopName }}-files:latest" + url: "$(image_repository)/{{ .WorkshopName }}-files:$(workshop_version)" includePaths: - /workshop/** - /exercises/** diff --git a/client-programs/pkg/templates/files/basic/workshop/content/00-workshop-overview.md b/client-programs/pkg/templates/files/classic/workshop/content/00-workshop-overview.md similarity index 100% rename from client-programs/pkg/templates/files/basic/workshop/content/00-workshop-overview.md rename to client-programs/pkg/templates/files/classic/workshop/content/00-workshop-overview.md diff --git a/client-programs/pkg/templates/files/basic/workshop/content/01-workshop-instructions.md b/client-programs/pkg/templates/files/classic/workshop/content/01-workshop-instructions.md similarity index 100% rename from client-programs/pkg/templates/files/basic/workshop/content/01-workshop-instructions.md rename to client-programs/pkg/templates/files/classic/workshop/content/01-workshop-instructions.md diff --git a/client-programs/pkg/templates/files/basic/workshop/content/99-workshop-summary.md b/client-programs/pkg/templates/files/classic/workshop/content/99-workshop-summary.md similarity index 100% rename from client-programs/pkg/templates/files/basic/workshop/content/99-workshop-summary.md rename to client-programs/pkg/templates/files/classic/workshop/content/99-workshop-summary.md diff --git a/client-programs/pkg/templates/files/basic/workshop/modules.yaml b/client-programs/pkg/templates/files/classic/workshop/modules.yaml similarity index 100% rename from client-programs/pkg/templates/files/basic/workshop/modules.yaml rename to client-programs/pkg/templates/files/classic/workshop/modules.yaml diff --git a/client-programs/pkg/templates/files/basic/workshop/workshop.yaml b/client-programs/pkg/templates/files/classic/workshop/workshop.yaml similarity index 100% rename from client-programs/pkg/templates/files/basic/workshop/workshop.yaml rename to client-programs/pkg/templates/files/classic/workshop/workshop.yaml diff --git a/client-programs/pkg/templates/files/hugo/README.md b/client-programs/pkg/templates/files/hugo/README.md new file mode 100644 index 00000000..d1109ae8 --- /dev/null +++ b/client-programs/pkg/templates/files/hugo/README.md @@ -0,0 +1,3 @@ +# {{ or .WorkshopTitle "Workshop" }} + +{{ or .WorkshopDescription "Workshop description" }} diff --git a/client-programs/pkg/templates/files/hugo/resources/workshop.yaml b/client-programs/pkg/templates/files/hugo/resources/workshop.yaml new file mode 100644 index 00000000..5ec268c6 --- /dev/null +++ b/client-programs/pkg/templates/files/hugo/resources/workshop.yaml @@ -0,0 +1,37 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: "{{ .WorkshopName }}" +spec: + title: "{{ or .WorkshopTitle "Workshop" }}" + description: "{{ or .WorkshopDescription "Workshop description." }}" + publish: + image: "$(image_repository)/{{ .WorkshopName }}-files:$(workshop_version)" + workshop: + {{- if .WorkshopImage }} + image: "{{ .WorkshopImage }}" + {{- end }} + files: + - image: + url: "$(image_repository)/{{ .WorkshopName }}-files:$(workshop_version)" + includePaths: + - /workshop/** + - /exercises/** + - /README.md + session: + namespaces: + budget: medium + applications: + terminal: + enabled: true + layout: split + editor: + enabled: true + console: + enabled: false + docker: + enabled: false + registry: + enabled: false + vcluster: + enabled: false diff --git a/client-programs/pkg/templates/files/hugo/workshop/config.yaml b/client-programs/pkg/templates/files/hugo/workshop/config.yaml new file mode 100644 index 00000000..19462409 --- /dev/null +++ b/client-programs/pkg/templates/files/hugo/workshop/config.yaml @@ -0,0 +1,31 @@ +# pathways: +# default: workshop +# +# paths: +# workshop: +# title: "Workshop" +# +# steps: +# - 00-workshop-overview +# - 01-workshop-instructions +# - 99-workshop-summary +# +# params: +# - name: NAME +# value: undefined +# aliases: +# - ALIAS + +# modules: +# - name: 00-workshop-overview +# title: Workshop Overview +# - name: 01-workshop-instructions +# title: Workshop Instructions +# - name: 99-workshop-summary +# title: Workshop Summary + +# params: +# - name: NAME +# value: undefined +# aliases: +# - ALIAS diff --git a/client-programs/pkg/templates/files/hugo/workshop/content/00-workshop-overview.md b/client-programs/pkg/templates/files/hugo/workshop/content/00-workshop-overview.md new file mode 100644 index 00000000..6f1b253a --- /dev/null +++ b/client-programs/pkg/templates/files/hugo/workshop/content/00-workshop-overview.md @@ -0,0 +1,5 @@ +--- +title: Workshop Overview +--- + +This is the initial landing page for your workshop. Include in this page a description of what your workshop is about. diff --git a/client-programs/pkg/templates/files/hugo/workshop/content/01-workshop-instructions.md b/client-programs/pkg/templates/files/hugo/workshop/content/01-workshop-instructions.md new file mode 100644 index 00000000..313f51f2 --- /dev/null +++ b/client-programs/pkg/templates/files/hugo/workshop/content/01-workshop-instructions.md @@ -0,0 +1,5 @@ +--- +title: Workshop Instructions +--- + +This is the first page of the workshop instructions, create as many separate pages as you need to. If necessary pages can be located in sub directories to provided grouping. diff --git a/client-programs/pkg/templates/files/hugo/workshop/content/99-workshop-summary.md b/client-programs/pkg/templates/files/hugo/workshop/content/99-workshop-summary.md new file mode 100644 index 00000000..dfd00c37 --- /dev/null +++ b/client-programs/pkg/templates/files/hugo/workshop/content/99-workshop-summary.md @@ -0,0 +1,5 @@ +--- +title: Workshop Summary +--- + +This is the last page of the workshop. Include in this page a summary of the workshop and any links to resources relevant to the workshop. This ensures anyone doing the workshop has material they can research later to learn more. diff --git a/developer-docs/README.md b/developer-docs/README.md new file mode 100644 index 00000000..5c27a0ee --- /dev/null +++ b/developer-docs/README.md @@ -0,0 +1,21 @@ +Developer Documentation +======================= + +The Educates project consists of the following Git repositories: + +* Educates Training Platform - https://github.com/vmware-tanzu-labs/educates-training-platform +* Educates Packages Repository - https://github.com/vmware-tanzu-labs/educates-packages +* Educates User Documentation - https://github.com/vmware-tanzu-labs/educates-docs +* Educates GitHub Actions - https://github.com/vmware-tanzu-labs/educates-github-actions + +Educates Training Platform (this repository), holds all source code for building and making releases of the core platform. + +Educates Packages Repository holds the definitions used to generate the Carvel packages from which Educates can be installed using the Carvel ``kapp-controller`` operator. + +Educates User Documentation holds the source files for user documentation hosted on https://docs.educates.dev/. + +Educates GitHub Actions holds GitHub actions to assist in publishing workshops to GitHub container registry. + +If wanting to contribute to Educates, you can build and deploy a local copy of Educates by following the [build instructions](build-instructions.md). + +For details on the design of Educates and how it works check out notes on it's [platform architecture](platform-architecture.md). diff --git a/developer-docs/build-instructions.md b/developer-docs/build-instructions.md new file mode 100644 index 00000000..c25ce638 --- /dev/null +++ b/developer-docs/build-instructions.md @@ -0,0 +1,4 @@ +Build Instructions +================== + +... diff --git a/developer-docs/platform-architecture.md b/developer-docs/platform-architecture.md new file mode 100644 index 00000000..76dee345 --- /dev/null +++ b/developer-docs/platform-architecture.md @@ -0,0 +1,4 @@ +Platform Architecture +===================== + +... diff --git a/image-cache/Dockerfile b/image-cache/Dockerfile new file mode 100644 index 00000000..8e06dfa9 --- /dev/null +++ b/image-cache/Dockerfile @@ -0,0 +1,32 @@ +#syntax=docker/dockerfile:1.3-labs + +FROM fedora:36 + +ARG TARGETARCH + +RUN useradd -u 1001 -g 0 -M -d /opt/app-root/src default && \ + mkdir -p /opt/app-root/src && \ + chown -R 1001:0 /opt/app-root + +WORKDIR /opt/app-root/src + +RUN <=0.34.0 @@ -8,7 +8,7 @@ django-oauth-toolkit==1.7.1 django-cors-middleware==1.5.0 requests==2.31.0 django-csp==3.7 -kopf[full-auth]==1.35.6 +kopf[full-auth]==1.36.1 pykube-ng==22.9.0 rstr==3.2.0 @@ -18,7 +18,8 @@ pip-tools==6.6.2 # Pin due to CVEs. Review if change django and django-oauth-toolkit versions. jwcrypto==1.4.2 oauthlib==3.2.2 -cryptography==41.0.0 -certifi==2022.12.07 +cryptography==41.0.3 +certifi==2023.07.22 wheel==0.38.1 sqlparse==0.4.4 +aiohttp==3.8.5 diff --git a/training-portal/requirements.txt b/training-portal/requirements.txt index 3d82869b..341e30ea 100644 --- a/training-portal/requirements.txt +++ b/training-portal/requirements.txt @@ -4,8 +4,10 @@ # # pip-compile requirements.in # -aiohttp==3.8.1 - # via kopf +aiohttp==3.8.5 + # via + # -r requirements.in + # kopf aiosignal==1.2.0 # via aiohttp asgiref==3.5.0 @@ -18,7 +20,7 @@ black==22.3.0 # via -r requirements.in cachetools==5.2.0 # via google-auth -certifi==2022.12.7 +certifi==2023.7.22 # via # -r requirements.in # kubernetes @@ -36,13 +38,13 @@ click==8.1.2 # pip-tools confusable-homoglyphs==3.2.0 # via django-registration -cryptography==41.0.0 +cryptography==41.0.3 # via # -r requirements.in # jwcrypto deprecated==1.2.13 # via jwcrypto -django==3.2.19 +django==3.2.20 # via # -r requirements.in # django-csp @@ -74,7 +76,7 @@ jwcrypto==1.4.2 # via # -r requirements.in # django-oauth-toolkit -kopf[full-auth]==1.35.6 +kopf[full-auth]==1.36.1 # via -r requirements.in kubernetes==24.2.0 # via kopf diff --git a/training-portal/src/project/apps/workshops/admin.py b/training-portal/src/project/apps/workshops/admin.py index f07f080e..3c722a45 100644 --- a/training-portal/src/project/apps/workshops/admin.py +++ b/training-portal/src/project/apps/workshops/admin.py @@ -38,7 +38,8 @@ class TrainingPortalAdmin(admin.ModelAdmin): "default_overtime", "default_deadline", "default_orphaned", - "default_overdue" + "default_overdue", + "default_refresh", "default_registry", "default_env", "update_workshop", @@ -86,12 +87,14 @@ class EnvironmentAdmin(admin.ModelAdmin): "workshop_link", "name", "uid", + "created_at", "position", "expires", "overtime", "deadline", "orphaned", "overdue", + "refresh", "capacity", "state", "reserved", @@ -111,10 +114,10 @@ def has_change_permission(self, request, obj=None): return False actions = [ - "recyle_environments", + "refresh_environments", ] - def recyle_environments(self, request, queryset): + def refresh_environments(self, request, queryset): for environment in queryset: if environment.state not in ( EnvironmentState.STOPPING, @@ -130,7 +133,7 @@ def recyle_environments(self, request, queryset): replace_workshop_environment(environment) - recyle_environments.short_description = "Recycle Environments" + refresh_environments.short_description = "Refresh Environments" class SessionAdmin(admin.ModelAdmin): diff --git a/training-portal/src/project/apps/workshops/manager/cleanup.py b/training-portal/src/project/apps/workshops/manager/cleanup.py index a05a9e9c..bff7eea2 100644 --- a/training-portal/src/project/apps/workshops/manager/cleanup.py +++ b/training-portal/src/project/apps/workshops/manager/cleanup.py @@ -83,9 +83,15 @@ def purge_expired_workshop_sessions(): elif session.environment.orphaned: try: # Query the idle time from the workshop session instance. + # Use the internal Kubernetes service for accessing the + # workshop instance as will fail if use public ingress and + # using a self signed CA as not currently injected such a + # CA into the training portal pod. - host = f"{session.name}.{settings.INGRESS_DOMAIN}" - url = f"{settings.INGRESS_PROTOCOL}://{host}/session/activity" + # host = f"{session.name}.{settings.INGRESS_DOMAIN}" + # url = f"{settings.INGRESS_PROTOCOL}://{host}/session/activity" + + url = f"http://{session.name}.{session.environment.name}:10080/session/activity" response = requests.get(url) diff --git a/training-portal/src/project/apps/workshops/manager/environments.py b/training-portal/src/project/apps/workshops/manager/environments.py index 1db412ea..014c3c77 100644 --- a/training-portal/src/project/apps/workshops/manager/environments.py +++ b/training-portal/src/project/apps/workshops/manager/environments.py @@ -14,6 +14,7 @@ from django.db import transaction from django.conf import settings +from django.utils import timezone from .resources import ResourceBody from .operator import background_task @@ -329,6 +330,24 @@ def delete_workshop_environment(environment): traceback.print_exc() +@background_task +@resources_lock +@transaction.atomic +def refresh_workshop_environments(training_portal): + """Looks for workshop environments which have a refresh interval and if that + interval has been exceeded shutdown the old workshop environment and create + a new in it's place using the same workshop definition. + + """ + + for environment in training_portal.running_environments(): + if environment.refresh.total_seconds() != 0: + duration = timezone.now() - environment.created_at + + if duration.total_seconds() > environment.refresh.total_seconds(): + replace_workshop_environment(environment) + + @background_task @resources_lock @transaction.atomic @@ -363,6 +382,7 @@ def update_workshop_environments(training_portal, workshops): environment.deadline = duration_as_timedelta(workshop["deadline"]) environment.orphaned = duration_as_timedelta(workshop["orphaned"]) environment.overdue = duration_as_timedelta(workshop["overdue"]) + environment.refresh = duration_as_timedelta(workshop["refresh"]) # Only update initial reserved session count if the workshop # environment hasn't actually been provisioned yet. @@ -399,6 +419,7 @@ def process_workshop_environment(portal, workshop, position): environment_deadline = duration_as_timedelta(workshop["deadline"]) environment_orphaned = duration_as_timedelta(workshop["orphaned"]) environment_overdue = duration_as_timedelta(workshop["overdue"]) + environment_refresh = duration_as_timedelta(workshop["refresh"]) if environment_deadline < environment_expires: environment_deadline = environment_expires @@ -415,6 +436,7 @@ def process_workshop_environment(portal, workshop, position): deadline=environment_deadline, orphaned=environment_orphaned, overdue=environment_overdue, + refresh=environment_refresh, registry=workshop["registry"], env=workshop["env"], ) @@ -467,6 +489,7 @@ def process_workshop_environment(portal, workshop, position): "environment": {"objects": [], "secrets": []}, "registry": environment.registry or None, "theme": {"name": settings.THEME_NAME}, + "cookies": {"domain": settings.SESSION_COOKIE_DOMAIN}, }, } @@ -540,6 +563,7 @@ def replace_workshop_environment(environment): "deadline": int(environment.deadline.total_seconds()), "orphaned": int(environment.orphaned.total_seconds()), "overdue": int(environment.overdue.total_seconds()), + "refresh": int(environment.refresh.total_seconds()), "registry": environment.registry, "env": environment.env, } diff --git a/training-portal/src/project/apps/workshops/manager/portal.py b/training-portal/src/project/apps/workshops/manager/portal.py index d6497289..80d49277 100644 --- a/training-portal/src/project/apps/workshops/manager/portal.py +++ b/training-portal/src/project/apps/workshops/manager/portal.py @@ -27,6 +27,7 @@ initiate_workshop_environments, shutdown_workshop_environments, delete_workshop_environments, + refresh_workshop_environments, update_environment_status, process_workshop_environment, replace_workshop_environment, @@ -134,6 +135,7 @@ def workshop_configuration(portal, workshop): workshop.setdefault("deadline", portal.default_deadline) workshop.setdefault("orphaned", portal.default_orphaned) workshop.setdefault("overdue", portal.default_overdue) + workshop.setdefault("refresh", portal.default_refresh) if workshop["deadline"] == "0": workshop["deadline"] = workshop["expires"] @@ -230,6 +232,7 @@ def process_training_portal(resource): default_deadline = "0" default_orphaned = "0" default_overdue = "0" + default_refresh = "0" default_capacity = spec.get("portal.capacity", default_capacity) default_reserved = spec.get("portal.reserved", default_reserved) @@ -237,6 +240,7 @@ def process_training_portal(resource): default_expires = spec.get("portal.expires", default_expires) default_orphaned = spec.get("portal.orphaned", default_orphaned) default_overdue = spec.get("portal.overdue", default_overdue) + default_refresh = spec.get("portal.refresh", default_refresh) default_capacity = spec.get("portal.workshop.defaults.capacity", default_capacity) default_reserved = spec.get("portal.workshop.defaults.reserved", default_reserved) @@ -246,6 +250,7 @@ def process_training_portal(resource): default_deadline = spec.get("portal.workshop.defaults.deadline", default_deadline) default_orphaned = spec.get("portal.workshop.defaults.orphaned", default_orphaned) default_overdue = spec.get("portal.workshop.defaults.overdue", default_overdue) + default_refresh = spec.get("portal.workshop.defaults.refresh", default_refresh) portal.default_capacity = default_capacity portal.default_reserved = default_reserved @@ -255,6 +260,7 @@ def process_training_portal(resource): portal.default_deadline = default_deadline portal.default_orphaned = default_orphaned portal.default_overdue = default_overdue + portal.default_refresh = default_refresh portal.default_registry = dict(spec.get("portal.workshop.defaults.registry", {})) @@ -330,6 +336,11 @@ def start_reconciliation_task(name): terminate_reserved_sessions(portal).schedule() + # Queue further task to look for workshop environments that should be + # retired and replaced with a new one. + + refresh_workshop_environments(portal).schedule() + # Queue further task to look for where additional workshop sessions need # to be created in reserved as required reserved sessions or capacity of # workshop environment or training portal was changed. diff --git a/training-portal/src/project/apps/workshops/manager/sessions.py b/training-portal/src/project/apps/workshops/manager/sessions.py index 69824665..cb451801 100644 --- a/training-portal/src/project/apps/workshops/manager/sessions.py +++ b/training-portal/src/project/apps/workshops/manager/sessions.py @@ -199,6 +199,9 @@ def create_workshop_session(session): # Prepare the body of the resource describing the workshop session. + characters = string.ascii_letters + string.digits + config_password = "".join(random.sample(characters, 32)) + session_body = { "apiVersion": f"training.{settings.OPERATOR_API_GROUP}/v1beta1", "kind": "WorkshopSession", @@ -220,11 +223,21 @@ def create_workshop_session(session): ], }, "spec": { + "workshop": { + "name": session.environment.workshop_name, + }, + "portal": { + "name": settings.PORTAL_NAME, + "url": portal_url, + }, "environment": {"name": session.environment.name}, "session": { "id": session.id, "username": "", "password": "", + "config": { + "password": config_password, + }, "ingress": { "domain": settings.INGRESS_DOMAIN, "secret": settings.INGRESS_SECRET, @@ -262,6 +275,7 @@ def create_workshop_session(session): resource.create() session.uid = resource.obj["metadata"]["uid"] + session.password = config_password # Update and save the state of the workshop session database record to # indicate it is running or waiting for confirmation on being activated if diff --git a/training-portal/src/project/apps/workshops/migrations/0004_auto_20230628_0400.py b/training-portal/src/project/apps/workshops/migrations/0004_auto_20230628_0400.py new file mode 100644 index 00000000..fbddbd86 --- /dev/null +++ b/training-portal/src/project/apps/workshops/migrations/0004_auto_20230628_0400.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.19 on 2023-06-28 04:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workshops', '0003_auto_20230513_0629'), + ] + + operations = [ + migrations.AddField( + model_name='session', + name='password', + field=models.CharField(blank=True, max_length=256, null=True, verbose_name='config password'), + ), + migrations.AlterField( + model_name='session', + name='token', + field=models.CharField(blank=True, max_length=256, null=True, verbose_name='activation token'), + ), + ] diff --git a/training-portal/src/project/apps/workshops/migrations/0005_auto_20230718_0356.py b/training-portal/src/project/apps/workshops/migrations/0005_auto_20230718_0356.py new file mode 100644 index 00000000..7d1fba77 --- /dev/null +++ b/training-portal/src/project/apps/workshops/migrations/0005_auto_20230718_0356.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.20 on 2023-07-18 03:56 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workshops', '0004_auto_20230628_0400'), + ] + + operations = [ + migrations.AddField( + model_name='environment', + name='refresh', + field=models.DurationField(default=datetime.timedelta(0), verbose_name='refresh interval'), + ), + migrations.AddField( + model_name='trainingportal', + name='default_refresh', + field=models.CharField(default='', max_length=32, verbose_name='default refresh'), + ), + ] diff --git a/training-portal/src/project/apps/workshops/migrations/0006_environment_created_at.py b/training-portal/src/project/apps/workshops/migrations/0006_environment_created_at.py new file mode 100644 index 00000000..89e01863 --- /dev/null +++ b/training-portal/src/project/apps/workshops/migrations/0006_environment_created_at.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.20 on 2023-07-18 04:07 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('workshops', '0005_auto_20230718_0356'), + ] + + operations = [ + migrations.AddField( + model_name='environment', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/training-portal/src/project/apps/workshops/models.py b/training-portal/src/project/apps/workshops/models.py index de9c80f8..905d70fd 100644 --- a/training-portal/src/project/apps/workshops/models.py +++ b/training-portal/src/project/apps/workshops/models.py @@ -90,6 +90,9 @@ class TrainingPortal(models.Model): default_overdue = models.CharField( verbose_name="default overdue", max_length=32, default="" ) + default_refresh = models.CharField( + verbose_name="default refresh", max_length=32, default="" + ) default_registry = JSONField(verbose_name="default registry", default={}) default_env = JSONField(verbose_name="default environment", default=[]) update_workshop = models.BooleanField( @@ -410,6 +413,7 @@ class Environment(models.Model): workshop = models.ForeignKey(Workshop, null=True, on_delete=models.PROTECT) name = models.CharField(verbose_name="environment name", max_length=255, default="") uid = models.CharField(verbose_name="resource uid", max_length=255, default="") + created_at = models.DateTimeField(auto_now_add=True) state = models.IntegerField( choices=EnvironmentState.choices(), default=EnvironmentState.STARTING ) @@ -432,6 +436,9 @@ class Environment(models.Model): overdue = models.DurationField( verbose_name="startup timeout", default=timedelta() ) + refresh = models.DurationField( + verbose_name="refresh interval", default=timedelta() + ) registry = JSONField(verbose_name="registry override", default={}) env = JSONField(verbose_name="environment overrides", default=[]) tally = models.IntegerField(verbose_name="workshop tally", default=0) @@ -612,9 +619,10 @@ class Session(models.Model): created = models.DateTimeField(null=True, blank=True) started = models.DateTimeField(null=True, blank=True) expires = models.DateTimeField(null=True, blank=True) - token = models.CharField(max_length=256, null=True, blank=True) + token = models.CharField(verbose_name="activation token", max_length=256, null=True, blank=True) url = models.URLField(verbose_name="session url", null=True) params = JSONField(verbose_name="session params", default={}) + password = models.CharField(verbose_name="config password", max_length=256, null=True, blank=True) def environment_name(self): return self.environment.name diff --git a/training-portal/src/project/apps/workshops/urls.py b/training-portal/src/project/apps/workshops/urls.py index 3775a6d3..261b1f87 100644 --- a/training-portal/src/project/apps/workshops/urls.py +++ b/training-portal/src/project/apps/workshops/urls.py @@ -37,6 +37,11 @@ "session//authorize/", views.session_authorize, name="workshops_session_authorize", + ), + path( + "session//config/", + views.session_config, + name="workshops_session_config", ), path( "session//schedule/", diff --git a/training-portal/src/project/apps/workshops/views/environment.py b/training-portal/src/project/apps/workshops/views/environment.py index 7c90e371..d0baa65b 100644 --- a/training-portal/src/project/apps/workshops/views/environment.py +++ b/training-portal/src/project/apps/workshops/views/environment.py @@ -196,6 +196,20 @@ def environment_request(request, name): if last_name: user_details["last_name"] = last_name + # The timeout here in seconds is how long the workshop session will be + # retained while waiting for it to be activated as a result of the URL + # returned by the REST API call being visited by a user. This technically + # could be set much higher if for example a frontend portal didn't return + # the URL to a user immediately, but instead waited to see if the workshop + # session was actually ready by making requests against it to get the + # configuration. Not that the URL should be visited before any startup + # timeout for a workshop session expires otherwise would fail at that + # point as the workshop session would have been deleted. As a result, if + # it is known that a workshop session takes a long time to be ready, then + # the startup timeout should be set a bit longer than this timeout. + + timeout = int(request.GET.get("timeout", "60").strip()) + # Extract any request parameters from the request body for using in late # binding of workshop session configuration. @@ -222,14 +236,14 @@ def environment_request(request, name): if not isinstance(request_params, list): return HttpResponseBadRequest("Malformed JSON request payload") - for value in request_params: - key = value.get("name", "") - item = value.get("item", "") + for item in request_params: + key = item.get("name", "") + value = item.get("value", "") if key: - if not isinstance(key, str) or not isinstance(item, str): + if not isinstance(key, str) or not isinstance(value, str): return HttpResponseBadRequest("Malformed JSON request payload") - + else: return HttpResponseBadRequest("Malformed JSON request payload") @@ -257,7 +271,7 @@ def environment_request(request, name): characters = string.ascii_letters + string.digits token = "".join(random.sample(characters, 32)) - session = retrieve_session_for_user(instance, user, token, None, params) + session = retrieve_session_for_user(instance, user, token, timeout, params) if not session: return JsonResponse({"error": "No session available"}, status=503) diff --git a/training-portal/src/project/apps/workshops/views/session.py b/training-portal/src/project/apps/workshops/views/session.py index 7c99bacb..8fec2993 100644 --- a/training-portal/src/project/apps/workshops/views/session.py +++ b/training-portal/src/project/apps/workshops/views/session.py @@ -9,6 +9,7 @@ "session_delete", "session_terminate", "session_authorize", + "session_config", "session_schedule", "session_extend", "session_event", @@ -286,6 +287,41 @@ def session_authorize(request, name): ) +@protected_resource() +@require_http_methods(["GET"]) +def session_config(request, name): + """Returns details for accessing the workshop session config.""" + + # XXX What if the portal configuration doesn't exist as process + # hasn't been initialized yet. Should return error indicating the + # service is not available. + + portal = TrainingPortal.objects.get(name=settings.TRAINING_PORTAL) + + # Ensure that the session exists. + + instance = portal.allocated_session(name) + + if not instance: + raise Http404("Session does not exist") + + # Check that are owner of session, a robot account, or a staff member. + + if ( + not request.user.is_staff + and not request.user.groups.filter(name="robots").exists() + ): + if instance.owner != request.user: + return HttpResponseForbidden("Access to session not permitted") + + details = {} + + details["url"] = instance.url + details["password"] = instance.password + + return JsonResponse(details) + + @protected_resource() @require_http_methods(["GET"]) def session_schedule(request, name): diff --git a/training-portal/src/project/settings.py b/training-portal/src/project/settings.py index f75809ed..c42b7ac6 100644 --- a/training-portal/src/project/settings.py +++ b/training-portal/src/project/settings.py @@ -252,12 +252,17 @@ CSRF_COOKIE_SAMESITE = "None" CSRF_COOKIE_SECURE = True +if os.getenv("SESSION_COOKIE_DOMAIN"): + SESSION_COOKIE_DOMAIN = os.getenv("SESSION_COOKIE_DOMAIN") + SESSION_COOKIE_NAME = f"sessionid-{PORTAL_NAME}" + OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth2_provider.Application" OAUTH2_PROVIDER = { "SCOPES": { "user:info": "User information", }, + "ACCESS_TOKEN_EXPIRE_SECONDS": 36000, "REFRESH_TOKEN_EXPIRE_SECONDS": 3600, } diff --git a/training-portal/testing/test-rest-api.http b/training-portal/testing/test-rest-api.http index 70553996..6ec73947 100644 --- a/training-portal/testing/test-rest-api.http +++ b/training-portal/testing/test-rest-api.http @@ -1,11 +1,11 @@ # Note: Set "rest-client.rememberCookiesForSubsequentRequests: false" -@rest_api_host=https://educates-cli-ui.educates-local-dev.xyz +@rest_api_host=https://educates-cli-ui.educates-local-dev.test @username=robot@educates @password=my-pasword @client_id=my-client-id @client_secret=my-client-secret -@environment_name=educates-cli-w01 +@environment_name=educates-cli-w02 @index_url=https://www.example.com/ ### @@ -44,13 +44,14 @@ POST {{rest_api_host}}/workshops/environment/{{environment_name}}/request/ &email=grumpy@me.com &first_name=Grumpy &last_name=Old Man + &timeout=300 Authorization: Bearer {{login.response.body.access_token}} Content-Type: application/json { "parameters": [ { - "name": "PARAM1", + "name": "WORKSHOP_USERNAME", "value": "VALUE1" } ] @@ -71,6 +72,24 @@ GET {{rest_api_host}}{{request2.response.body.url}} ### +# @name config +GET {{rest_api_host}}/workshops/session/{{request1.response.body.name}}/config/ +Authorization: Bearer {{login.response.body.access_token}} + +### + +# @name environment +GET {{config.response.body.url}}/config/environment?token={{config.response.body.password}} +Authorization: Bearer {{login.response.body.access_token}} + +### + +# @name variables +GET {{config.response.body.url}}/config/variables?token={{config.response.body.password}} +Authorization: Bearer {{login.response.body.access_token}} + +### + # @name sessions GET {{rest_api_host}}/workshops/user/{{request1.response.body.user}}/sessions/ Authorization: Bearer {{login.response.body.access_token}} diff --git a/tunnel-manager/requirements.txt b/tunnel-manager/requirements.txt index 868cb115..1dfca059 100644 --- a/tunnel-manager/requirements.txt +++ b/tunnel-manager/requirements.txt @@ -1,5 +1,5 @@ -kopf[full-auth]==1.36.0 -aiohttp==3.8.4 +kopf[full-auth]==1.36.1 +aiohttp==3.8.5 PyYAML==6.0 pykube-ng==22.9.0 websockets==10.4 diff --git a/workshop-images/base-environment/CREDITS.md b/workshop-images/base-environment/CREDITS.md deleted file mode 100644 index ca90efdc..00000000 --- a/workshop-images/base-environment/CREDITS.md +++ /dev/null @@ -1,8 +0,0 @@ -The source code for the workshop base environment image initially borrowed code -from and was modelled after a similar project by the same author (Graham -Dumpleton) called [Homeroom](https://github.com/openshift-homeroom), which was -developed while working for Red Hat. The current source code for the workshop -base environment image is a major refactoring and rewrite of the prior work, -with a primary focus on native Kubernetes clusters instead of OpenShift. The -original workshop base environment image code was made available under the terms -of the Apache License, Version 2.0. diff --git a/workshop-images/base-environment/opt/eduk8s/bin/download-assets b/workshop-images/base-environment/opt/eduk8s/bin/download-assets index 03337ca2..55651db6 100755 --- a/workshop-images/base-environment/opt/eduk8s/bin/download-assets +++ b/workshop-images/base-environment/opt/eduk8s/bin/download-assets @@ -43,6 +43,9 @@ done # original permissions exist. To try and avoid problems, if we see that only # owner permissions exist, copy those to group and others. There is still a risk # that a source file may not have actually had group or other permissions and so -# that is desired, but not likely and nothing else that can be done. +# that is desired, but not likely and nothing else that can be done. Also note +# that we don't fail if there are problems. In particular, if a volume mount was +# used to add files, the directories will not be writable so we want to avoid a +# hard failure in that case. -chmod -R go=u-w * +chmod -R go=u-w * || true diff --git a/workshop-images/base-environment/opt/eduk8s/bin/download-packages b/workshop-images/base-environment/opt/eduk8s/bin/download-packages index 6a1f55cc..e7da393f 100755 --- a/workshop-images/base-environment/opt/eduk8s/bin/download-packages +++ b/workshop-images/base-environment/opt/eduk8s/bin/download-packages @@ -29,12 +29,22 @@ fi time vendir sync -f vendir.yaml --lock-file vendir.lock.yaml $VENDIR_ARGS +# As a workaround for vendir not preserving file mode bits when unpacking +# tarballs downloaded via HTTP, always set setup.d scripts to be executable. +# Note that packages will need to provide a setup.d script to fix up file mode +# bits on other files, such as in a bin directory. + +chmod +x */setup.d/*.sh || true + # When using imgpkg push/pull, it does not preserve permissions for groups and # other and instead only keeps user permissions. This will break workshops where # files are being used with docker builds and things will only work where # original permissions exist. To try and avoid problems, if we see that only # owner permissions exist, copy those to group and others. There is still a risk # that a source file may not have actually had group or other permissions and so -# that is desired, but not likely and nothing else that can be done. +# that is desired, but not likely and nothing else that can be done. Also note +# that we don't fail if there are problems. In particular, if a volume mount was +# used to add files, the directories will not be writable so we want to avoid a +# hard failure in that case. -chmod -R go=u-w * +chmod -R go=u-w * || true diff --git a/workshop-images/base-environment/opt/eduk8s/bin/merge-workshop b/workshop-images/base-environment/opt/eduk8s/bin/merge-workshop index 587d81c3..a8981c29 100755 --- a/workshop-images/base-environment/opt/eduk8s/bin/merge-workshop +++ b/workshop-images/base-environment/opt/eduk8s/bin/merge-workshop @@ -25,3 +25,10 @@ else cp -rp $SOURCE_DIRECTORY/* $WORKSPACE_DIRECTORY/ rm -rf $WORKSPACE_DIRECTORY/workshop fi + +# As a workaround for vendir not preserving file mode bits when unpacking +# tarballs downloaded via HTTP, always set setup.d scripts to be executable. +# Note that a workshop will need to provide a setup.d script to fix up file mode +# bits on other files, such as in a bin directory. + +chmod +x $ALTERNATE_DIRECTORY/setup.d/*.sh || true diff --git a/workshop-images/base-environment/opt/eduk8s/bin/rebuild-content b/workshop-images/base-environment/opt/eduk8s/bin/rebuild-content new file mode 100755 index 00000000..4fc72b95 --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/bin/rebuild-content @@ -0,0 +1,83 @@ +#!/bin/bash + +set -x +set -eo pipefail + +# Create snapshot of workshop environment variables. + +jq -n env > $HOME/.local/share/workshop/workshop-environment.json + +# Create snapshot of workshop parameters for instructions. + +YTT_ARGS=() + +WORKSHOP_TITLE=$(workshop-definition -r '(.spec.title // "Workshop")') +WORKSHOP_DESCRIPTION=$(workshop-definition -r '(.spec.description // "")') + +YTT_ARGS+=(--data-value workshop_title="$WORKSHOP_TITLE") +YTT_ARGS+=(--data-value workshop_description="$WORKSHOP_DESCRIPTION") + +YTT_ARGS+=(--dangerous-allow-all-symlink-destinations) + +YTT_ARGS+=(--data-value-file ssh_private_key=$HOME/.ssh/id_rsa) +YTT_ARGS+=(--data-value-file ssh_public_key=$HOME/.ssh/id_rsa.pub) + +if [ -f /var/run/secrets/kubernetes.io/serviceaccount/token ]; then + YTT_ARGS+=(--data-value-file kubernetes_token=/var/run/secrets/kubernetes.io/serviceaccount/token) +fi + +if [ -f /var/run/secrets/kubernetes.io/serviceaccount/ca.crt ]; then + YTT_ARGS+=(--data-value-file kubernetes_ca_crt=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt) +fi + +YTT_ARGS+=(-f $HOME/.local/share/workshop/workshop-environment.json --file-mark workshop-environment.json:type=data) + +if [ -f $WORKSHOP_DIR/config.yaml ]; then + YTT_ARGS+=(-f $WORKSHOP_DIR/config.yaml --file-mark config.yaml:path=workshop-configuration.yaml --file-mark config.yaml:type=data) +fi + +ytt -f /opt/eduk8s/etc/templates/workshop-variables.yaml "${YTT_ARGS[@]}" -o json >$HOME/.local/share/workshop/workshop-variables.json + +# Bail out if not using the Hugo renderer for instructions. + +if [ x"$WORKSHOP_RENDERER" != x"local" ]; then + exit 0 +fi + +if [ -f $WORKSHOP_DIR/workshop.yaml -o -f $WORKSHOP_DIR/modules.yaml ]; then + exit 0 +fi + +YTT_ARGS=() + +YTT_ARGS+=(-f $HOME/.local/share/workshop/workshop-variables.json --file-mark workshop-variables.json:type=data) + +if [ -f $WORKSHOP_DIR/config.yaml ]; then + YTT_ARGS+=(-f $WORKSHOP_DIR/config.yaml --file-mark config.yaml:path=workshop-configuration.yaml --file-mark config.yaml:type=data) +fi + +# Generate Hugo configuration. + +ytt -f /opt/eduk8s/etc/templates/hugo-configuration.yaml "${YTT_ARGS[@]}" >$HOME/.local/share/workshop/hugo-configuration.yaml + +# Run Hugo to generate static HTML files. + +HUGO_ARGS=() + +HUGO_ARGS+=(--ignoreCache) +HUGO_ARGS+=(--cleanDestinationDir) + +HUGO_ARGS+=(--minify) + +HUGO_ARGS+=(--configDir $WORKSHOP_DIR/config) +HUGO_ARGS+=(--config $HOME/.local/share/workshop/hugo-configuration.yaml) + +HUGO_ARGS+=(--source $WORKSHOP_DIR) +HUGO_ARGS+=(--destination $WORKSHOP_DIR/public) + +HUGO_ARGS+=(--themesDir /opt/eduk8s/etc/themes) +HUGO_ARGS+=(--theme educates) + +HUGO_ARGS+=(--baseURL $INGRESS_PROTOCOL://$SESSION_NAMESPACE.$INGRESS_DOMAIN$INGRESS_PORT_SUFFIX/workshop/content/) + +hugo "${HUGO_ARGS[@]}" diff --git a/workshop-images/base-environment/opt/eduk8s/bin/rebuild-workshop b/workshop-images/base-environment/opt/eduk8s/bin/rebuild-workshop index ae6f0eea..7de0afe0 100755 --- a/workshop-images/base-environment/opt/eduk8s/bin/rebuild-workshop +++ b/workshop-images/base-environment/opt/eduk8s/bin/rebuild-workshop @@ -1,14 +1,24 @@ #!/bin/bash +# Run workshop specific setup scripts and then source the corresponding profile +# scripts as well so they are available for later setup scripts. Note that this +# script will be executed inline to the start-container script. In that case +# errors in the profile scripts can cause the workshop container to not start. +# Since we cannot capture what happens in profile scripts into the log file in +# that case, and because the container will not actually start anyway, it is +# necessary to look at the container log file instead. An error in setup scripts +# on the other hand shouldn't usually cause the container to not start and the +# log can be consulted from within the workshop session container. + set -x +cd $HOME + WORKSHOP_ENV=/tmp/workshop-env-$$.sh SETUP_LOGFILE=$HOME/.local/share/workshop/setup-scripts.log SETUP_FAILED=$HOME/.local/share/workshop/setup-scripts.failed -cd $HOME - rm -f $SETUP_FAILED touch $SETUP_LOGFILE @@ -109,3 +119,21 @@ for script in $HOME/workshop/profile.d/*.sh $HOME/workshop/profile.d/sh.local; d . "$script" fi done + +# Now rebuild the workshop content. In the case of Hugo this will include +# regenerating the static HTML files making up the workshop content. In other +# cases only the snapshot of the workshop environment and parameters is created. + +if [ x"$WORKSHOP_RENDERER" == x"local" ]; then + if [ x"$LOCAL_RENDERER_TYPE" == x"" ]; then + if [ -f $WORKSHOP_DIR/workshop.yaml -o -f $WORKSHOP_DIR/modules.yaml ]; then + LOCAL_RENDERER_TYPE="classic" + else + LOCAL_RENDERER_TYPE="hugo" + fi + fi +fi + +export LOCAL_RENDERER_TYPE + +rebuild-content || touch $SETUP_FAILED diff --git a/workshop-images/base-environment/opt/eduk8s/bin/workshop-definition b/workshop-images/base-environment/opt/eduk8s/bin/workshop-definition index 05fb4297..eb66ec67 100755 --- a/workshop-images/base-environment/opt/eduk8s/bin/workshop-definition +++ b/workshop-images/base-environment/opt/eduk8s/bin/workshop-definition @@ -2,11 +2,9 @@ if [ ! -f $HOME/.local/share/workshop/workshop-definition.json ]; then if [ -f /opt/eduk8s/config/workshop.yaml ]; then - cp /opt/eduk8s/config/workshop.yaml $HOME/.local/share/workshop/workshop-definition.yaml - ytt -f $HOME/.local/share/workshop/workshop-definition.yaml -o json >$HOME/.local/share/workshop/workshop-definition.json + cat /opt/eduk8s/config/workshop.yaml | ytt -f - -o json | jq >$HOME/.local/share/workshop/workshop-definition.json elif [ -f /opt/assets/files/resources/workshop.yaml ]; then - cp /opt/assets/files/resources/workshop.yaml $HOME/.local/share/workshop/workshop-definition.yaml - ytt -f $HOME/.local/share/workshop/workshop-definition.yaml -o json >$HOME/.local/share/workshop/workshop-definition.json + cat /opt/assets/files/resources/workshop.yaml | ytt -f - -o json | jq >$HOME/.local/share/workshop/workshop-definition.json else echo "{}" >$HOME/.local/share/workshop/workshop-definition.json fi diff --git a/workshop-images/base-environment/opt/eduk8s/etc/profile b/workshop-images/base-environment/opt/eduk8s/etc/profile index 2d4a3dad..fe68b1f3 100644 --- a/workshop-images/base-environment/opt/eduk8s/etc/profile +++ b/workshop-images/base-environment/opt/eduk8s/etc/profile @@ -54,7 +54,7 @@ if [ -f $HOME/.local/share/workshop/workshop-env-packages.sh ]; then set -a; . $HOME/.local/share/workshop/workshop-env-packages.sh; set +a fi -for i in /opt/packages/*/etc/profile.d/*.sh /opt/packages/*/etc/profile.d/sh.local; do +for i in /opt/packages/*/profile.d/*.sh /opt/packages/*/profile.d/sh.local; do if [ -r "$i" ]; then . "$i" >/dev/null fi diff --git a/workshop-images/base-environment/opt/eduk8s/etc/profile.d/02-workshop.sh b/workshop-images/base-environment/opt/eduk8s/etc/profile.d/02-workshop.sh index 1d451eb7..e25afe1d 100644 --- a/workshop-images/base-environment/opt/eduk8s/etc/profile.d/02-workshop.sh +++ b/workshop-images/base-environment/opt/eduk8s/etc/profile.d/02-workshop.sh @@ -9,10 +9,8 @@ if [ x"$ENABLE_WORKSHOP" != x"true" ]; then fi WORKSHOP_PORT=10082 -WORKSHOP_URL=${WORKSHOP_URL:-$(workshop-definition -r '(.spec.session.applications.workshop.url // "")')} export WORKSHOP_PORT -export WORKSHOP_URL # Work out location of the workshop content. This will be in workshop user home # directory if mounting local directory, or is a custom workshop image but files diff --git a/workshop-images/base-environment/opt/eduk8s/etc/setup.d/01-kubernetes.sh b/workshop-images/base-environment/opt/eduk8s/etc/setup.d/01-kubernetes.sh index d6e72031..d0035839 100755 --- a/workshop-images/base-environment/opt/eduk8s/etc/setup.d/01-kubernetes.sh +++ b/workshop-images/base-environment/opt/eduk8s/etc/setup.d/01-kubernetes.sh @@ -39,7 +39,7 @@ fi # to align the versions so that that warning doesn't happen all the time when # using kubectl from the command line. -KUBECTL_VERSION=$(kubectl version -o json | jq -re '[.serverVersion.major,.serverVersion.minor]|join(".")') +KUBECTL_VERSION=$(kubectl version -o json | jq -re '[.serverVersion.major,.serverVersion.minor]|join(".")' | sed -e 's/^\([1-9]*.[1-9]*\).*$/\1/') case "$KUBECTL_VERSION" in 1.2[012]) diff --git a/workshop-images/base-environment/opt/eduk8s/etc/supervisord.conf b/workshop-images/base-environment/opt/eduk8s/etc/supervisord.conf index 01024e0e..332e275f 100644 --- a/workshop-images/base-environment/opt/eduk8s/etc/supervisord.conf +++ b/workshop-images/base-environment/opt/eduk8s/etc/supervisord.conf @@ -147,4 +147,4 @@ serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket ; include files themselves. [include] -files = /opt/eduk8s/etc/supervisor/*.conf /opt/eduk8s/workshop/supervisor/*.conf /opt/packages/*/etc/supervisor/*.conf /opt/workshop/supervisor/*.conf /home/eduk8s/workshop/supervisor/*.conf +files = /opt/eduk8s/etc/supervisor/*.conf /opt/eduk8s/workshop/supervisor/*.conf /opt/packages/*/supervisor/*.conf /opt/workshop/supervisor/*.conf /home/eduk8s/workshop/supervisor/*.conf diff --git a/workshop-images/base-environment/opt/eduk8s/etc/templates/hugo-configuration.yaml b/workshop-images/base-environment/opt/eduk8s/etc/templates/hugo-configuration.yaml new file mode 100644 index 00000000..9833fa24 --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/templates/hugo-configuration.yaml @@ -0,0 +1,58 @@ +#@ load("@ytt:data", "data") +#@ load("@ytt:json", "json") +#@ load("@ytt:yaml", "yaml") +#@ load("@ytt:url", "url") + +#@ if "workshop-configuration.yaml" in data.list(): +#@ config = yaml.decode(data.read("workshop-configuration.yaml")) or {} +#@ else: +#@ config = {} +#@ end + +#@ params = json.decode(data.read("workshop-variables.json")) + +#@ pathways = config.get("pathways", {}).get("paths", {}) +#@ pathway_name = params.get("pathway_name", "") + +#@ if not pathway_name and pathways: +#@ pathway_name = config.get("pathways", {}).get("default", "") +#@ end + +#@ modules = config.get("pathways", {}).get("modules", {}) + +#@ path = pathways.get(pathway_name, {}) + +#@ steps = path.get("steps", []) + +#@ active_modules = {} + +#@ first_page = None +#@ prev_page = None + +#@ count = 1 + +#@ for step in steps: +#@ if first_page == None: +#@ first_page = step +#@ end +#@ module = modules.get(step, {}) +#@ module["path"] = step +#@ if prev_page != None: +#@ module["prev_page"] = prev_page +#@ active_modules[prev_page]["next_page"] = step +#@ else: +#@ module["prev_page"] = None +#@ end +#@ module["next_page"] = None +#@ module["step"] = count +#@ prev_page = step +#@ count = count + 1 +#@ active_modules[step] = module +#@ end + +#@ if first_page: +#@ params["__first_page__"] = first_page +#@ params["__modules__"] = active_modules +#@ end + +params: #@ params diff --git a/workshop-images/base-environment/opt/eduk8s/etc/templates/workshop-variables.yaml b/workshop-images/base-environment/opt/eduk8s/etc/templates/workshop-variables.yaml new file mode 100644 index 00000000..52ff22b0 --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/templates/workshop-variables.yaml @@ -0,0 +1,142 @@ +#@ load("@ytt:data", "data") +#@ load("@ytt:json", "json") +#@ load("@ytt:yaml", "yaml") +#@ load("@ytt:template", "template") + +#@ def xgetattr(object, path, default=None): +#@ def _lookup(object, key, default=None): +#@ keys = key.split(".") +#@ value = default +#@ for key in keys: +#@ value = getattr(object, key, None) +#@ if value == None: +#@ return default +#@ end +#@ object = value +#@ end +#@ return value +#@ end +#@ return _lookup(object, path, default) +#@ end + +#@ if "workshop-configuration.yaml" in data.list(): +#@ config = yaml.decode(data.read("workshop-configuration.yaml")) or {} +#@ else: +#@ config = {} +#@ end + +#@ environ = json.decode(data.read("workshop-environment.json")) + +#@ pathways = config.get("pathways", {}).get("paths", {}) +#@ pathway_name = environ.get("PATHWAY_NAME", "") + +#@ if not pathway_name and pathways: +#@ pathway_name = config.get("pathways", {}).get("default", "") +#@ end + +#@ path = pathways.get(pathway_name, {}) + +#@ workshop_title = xgetattr(data.values, "workshop_name", "Workshop") +#@ workshop_title = path.get("title", workshop_title) + +#@ workshop_description = xgetattr(data.values, "workshop_description", "") +#@ workshop_description = path.get("description", workshop_description) + +#@ params = {} + +#@ def add_param_from_environ(target, source=None, default=""): +#@ if source == None: +#@ source = target.upper() +#@ end +#@ if source in environ: +#@ params[target] = environ[source] +#@ elif default != None: +#@ params[target] = default +#@ end +#@ end + +#@ def add_param_from_data_value(target, source=None, default="", lstrip="", rstrip=""): +#@ if source == None: +#@ source = target +#@ end +#@ params[target] = xgetattr(data.values, source, default) +#@ params[target] = params[target].lstrip(lstrip).rstrip(rstrip) +#@ end + +#@ def add_param_from_data_file(target, source, default="", lstrip="", rstrip=""): +#@ if source in data.list(): +#@ params[target] = data.read(source) +#@ elif default != None: +#@ params[target] = default +#@ end +#@ params[target] = params[target].lstrip(lstrip).rstrip(rstrip) +#@ end + +#@ def add_params_from_config(items): +#@ for item in items: +#@ name = item["name"] +#@ value = item.get("value", "") +#@ aliases = item.get("aliases") +#@ +#@ if aliases != None: +#@ for alias in aliases: +#@ if environ.get(alias) != None: +#@ value = environ[alias] +#@ break +#@ end +#@ end +#@ else: +#@ if environ.get(name) != None: +#@ value = environ[name] +#@ end +#@ end +#@ params[name] = value +#@ end +#@ end + +#@ add_params_from_config(config.get("params", [])) +#@ add_params_from_config(path.get("params", [])) + +#@ params["pathway_name"] = pathway_name + +#@ params["workshop_title"] = workshop_title +#@ params["workshop_description"] = workshop_description + +#@ add_param_from_environ("google_tracking_id") +#@ add_param_from_environ("clarity_tracking_id") +#@ add_param_from_environ("amplitude_tracking_id") + +#@ add_param_from_environ("platform_arch") +#@ add_param_from_environ("image_repository") +#@ add_param_from_environ("oci_image_cache") +#@ add_param_from_environ("assets_repository") +#@ add_param_from_environ("workshop_name") +#@ add_param_from_environ("environment_name") +#@ add_param_from_environ("session_name") +#@ add_param_from_environ("session_id") +#@ add_param_from_environ("session_url") +#@ add_param_from_environ("session_namespace") +#@ add_param_from_environ("workshop_namespace") +#@ add_param_from_environ("training_portal") +#@ add_param_from_environ("session_hostname") +#@ add_param_from_environ("cluster_domain") +#@ add_param_from_environ("ingress_domain") +#@ add_param_from_environ("ingress_protocol") +#@ add_param_from_environ("ingress_port_suffix") +#@ add_param_from_environ("ingress_port") +#@ add_param_from_environ("ingress_class") +#@ add_param_from_environ("storage_class") +#@ add_param_from_environ("policy_engine") +#@ add_param_from_environ("policy_name") +#@ add_param_from_environ("services_password") +#@ add_param_from_environ("config_password") +#@ add_param_from_environ("kubernetes_api_url") + +#@ add_param_from_environ("restart_url") + +#@ add_param_from_data_value("ssh_private_key", rstrip="\n") +#@ add_param_from_data_value("ssh_public_key", rstrip="\n") +#@ add_param_from_data_value("kubernetes_token", rstrip="\n") +#@ add_param_from_data_value("kubernetes_ca_crt", rstrip="\n") + +_: #@ template.replace(params) diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/hugo.toml b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/hugo.toml new file mode 100644 index 00000000..938865a6 --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/hugo.toml @@ -0,0 +1 @@ +# Theme config. diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/404.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/404.html new file mode 100644 index 00000000..e69de29b diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/baseof.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/baseof.html new file mode 100644 index 00000000..c150cf16 --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/baseof.html @@ -0,0 +1,72 @@ + + + + + {{ partial "head.html" . }} + + +{{ .Scratch.Set "__modules__" (.Site.Param "__modules__") }} +{{ .Scratch.Set "__first_page__" (.Site.Param "__first_page__") }} + +{{ $modules := (.Scratch.Get "__modules__") }} +{{ $first_page := (.Scratch.Get "__first_page__") }} + +{{ $prev_page := "" }} + +{{ if not $modules }} +{{ $count := 1 }} +{{ $first_page = "" }} +{{ $by_weight := false }} +{{ $all_pages := sort .Site.RegularPages "File.Path" }} +{{ range $all_pages }} +{{ if .Weight }} +{{ $by_weight = true }} +{{ end }} +{{ end }} +{{ if $by_weight }} +{{ $all_pages = .Site.RegularPages }} +{{ end }} +{{ range $page := $all_pages }} +{{ $path := $page.File.Path }} +{{ $path = $path | replaceRE "^/?" "" }} +{{ $path = $path | replaceRE "/index\\.md$" "" }} +{{ $path = $path | replaceRE "\\.md$" "" }} +{{ if not $first_page }} +{{ $first_page = $path }} +{{ end }} +{{ $entry := (dict "path" $path "title" $page.Title "prev_page" $prev_page "step" $count) }} +{{ $modules = merge $modules (dict $path $entry) }} +{{ if $prev_page }} +{{ $item := index $modules $prev_page }} +{{ $item = merge $item (dict "next_page" $path) }} +{{ $modules = merge $modules (dict $prev_page $item) }} +{{ end }} +{{ $prev_page = $path }} +{{ $count = add $count 1}} +{{ end }} +{{ .Scratch.Set "__modules__" $modules }} +{{ .Scratch.Set "__first_page__" $first_page }} +{{ end }} + +{{ $current_module := (index $modules (trim (substr (.Page.RelPermalink) (len (relURL ""))) "/")) }} + + + + {{ partial "header.html" . }} + + {{ block "main" . }} + {{ end }} + + {{ partial "footer.html" . }} + + + + diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/index.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/index.html new file mode 100644 index 00000000..9e25e806 --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/index.html @@ -0,0 +1,17 @@ +{{ define "main" }} + +{{ $first_page := (.Scratch.Get "__first_page__") }} + +
+
+
+
+ +
+
+
+
+ +{{ end }} diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/list.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/list.html new file mode 100644 index 00000000..e69de29b diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/single.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/single.html new file mode 100644 index 00000000..a99756c7 --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/_default/single.html @@ -0,0 +1,41 @@ +{{ define "main" }} + +{{ $modules := (.Scratch.Get "__modules__") }} +{{ $current_module := (index $modules (trim (substr (.Page.RelPermalink) (len (relURL ""))) "/")) }} + +
+
+
+
+ {{ $title := $current_module.title }} + {{ if not $title }} + {{ $title = .Page.Title }} + {{ end }} + {{ if $title }} +

{{ $current_module.step }}: {{ $title }}

+ {{ end }} +
+ {{ .Content }} +
+ {{ if $current_module.next_page }} +
+
+ +
+
+ {{ else if and $current_module (.Param "restart_url") }} +
+
+ +
+
+ {{ end }} +
+
+
+
+ +{{ end }} diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/partials/footer.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/partials/footer.html new file mode 100644 index 00000000..c493c59a --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/partials/footer.html @@ -0,0 +1,2 @@ + + diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/partials/head.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/partials/head.html new file mode 100644 index 00000000..90587d60 --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/partials/head.html @@ -0,0 +1,34 @@ + + + +{{ block "title" . }} + {{ .Site.Title }} + {{ end }} + + + + + + + + + + +{{ if ne (.Param "google_tracking_id") "" }} + + +{{ end }} + +{{ if ne (.Param "clarity_tracking_id") "" }} + +{{ end }} diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/partials/header.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/partials/header.html new file mode 100644 index 00000000..772146ca --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/partials/header.html @@ -0,0 +1,80 @@ +{{ $modules := (.Scratch.Get "__modules__") }} +{{ $current_module := (index $modules (trim (substr (.Page.RelPermalink) (len (relURL ""))) "/")) }} + +{{ $site := .Site }} + + + + diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/copy.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/copy.html new file mode 100644 index 00000000..c3fb699c --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/copy.html @@ -0,0 +1 @@ + diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/danger.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/danger.html new file mode 100644 index 00000000..b60f4222 --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/danger.html @@ -0,0 +1,4 @@ +
+

{{ .Get "title" | default "Danger" }}

+

{{ printf "%s" .Inner | markdownify }}

+
diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/note.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/note.html new file mode 100644 index 00000000..f5833f63 --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/note.html @@ -0,0 +1,4 @@ +
+

{{ .Get "title" | default "Note" }}

+

{{ printf "%s" .Inner | markdownify }}

+
diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/pathway.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/pathway.html new file mode 100644 index 00000000..0b25c65d --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/pathway.html @@ -0,0 +1,3 @@ +{{ if eq $.Page.Site.Params.pathway_name (.Get 0) }} +{{- .Inner | markdownify }} +{{ end }} diff --git a/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/warning.html b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/warning.html new file mode 100644 index 00000000..f8d7142a --- /dev/null +++ b/workshop-images/base-environment/opt/eduk8s/etc/themes/educates/layouts/shortcodes/warning.html @@ -0,0 +1,4 @@ +
+

{{ .Get "title" | default "Warning" }}

+

{{ printf "%s" .Inner | markdownify }}

+
diff --git a/workshop-images/base-environment/opt/eduk8s/sbin/start-container b/workshop-images/base-environment/opt/eduk8s/sbin/start-container index 0ba97445..48acb921 100755 --- a/workshop-images/base-environment/opt/eduk8s/sbin/start-container +++ b/workshop-images/base-environment/opt/eduk8s/sbin/start-container @@ -24,7 +24,9 @@ TRAINING_PORTAL=${TRAINING_PORTAL:-workshop} ENVIRONMENT_NAME=${ENVIRONMENT_NAME:-workshop} WORKSHOP_NAMESPACE=${WORKSHOP_NAMESPACE:-workshop} SESSION_NAMESPACE=${SESSION_NAMESPACE:-workshop} +SESSION_NAME=${SESSION_NAME:-workshop} +SESSION_HOSTNAME=${SESSION_HOSTNAME:-workshop-127-0-0-1.nip.io} INGRESS_PROTOCOL=${INGRESS_PROTOCOL:-http} INGRESS_DOMAIN=${INGRESS_DOMAIN:-127-0-0-1.nip.io} INGRESS_PORT_SUFFIX=${INGRESS_PORT_SUFFIX-:10081} @@ -34,7 +36,9 @@ export TRAINING_PORTAL export ENVIRONMENT_NAME export WORKSHOP_NAMESPACE export SESSION_NAMESPACE +export SESSION_NAME +export SESSION_HOSTNAME export INGRESS_PROTOCOL export INGRESS_DOMAIN export INGRESS_PORT_SUFFIX @@ -190,31 +194,21 @@ export DEFAULT_PAGE # Work out what variation of the workshop renderer needs to be enabled. -WORKSHOP_RENDERER=${WORKSHOP_RENDERER:-$(workshop-definition -r '(.spec.session.applications.workshop.renderer // "")')} -WORKSHOP_URL=${WORKSHOP_URL:-$(workshop-definition -r '(.spec.session.applications.workshop.url // "")')} +WORKSHOP_RENDERER="local" -if [ x"$WORKSHOP_RENDERER" == x"" ]; then - if [ x"$WORKSHOP_URL" != x"" ]; then - if [[ "$WORKSHOP_URL" =~ ^(https?|\$\(ingress_protocol\))://.* ]]; then - WORKSHOP_RENDERER="remote" - else - WORKSHOP_RENDERER="local" - fi - else - WORKSHOP_RENDERER="local" - fi -fi +WORKSHOP_URL=$(workshop-definition -r '(.spec.session.applications.workshop.url // "")') +WORKSHOP_PROXY=$(workshop-definition -r '(.spec.session.applications.workshop.proxy // "")') +WORKSHOP_PATH=$(workshop-definition -r '(.spec.session.applications.workshop.path // "")') -case "$WORKSHOP_RENDERER" in - local|remote|static) - ;; - *) - WORKSHOP_RENDERER="local" - ;; -esac +if [ x"$WORKSHOP_URL" != x"" ]; then + WORKSHOP_RENDERER="remote" +elif [ x"$WORKSHOP_PROXY" != x"" ]; then + WORKSHOP_RENDERER="proxy" +elif [ x"$WORKSHOP_PATH" != x"" ]; then + WORKSHOP_RENDERER="static" +fi export WORKSHOP_RENDERER -export WORKSHOP_URL # Save away all the environment variables which we need to be available later # if an SSH terminal session is created. This needs to include some environment @@ -231,6 +225,8 @@ WORKSHOP_NAMESPACE="$WORKSHOP_NAMESPACE" SESSION_NAMESPACE="$SESSION_NAMESPACE" SESSION_NAME="$SESSION_NAME" SESSION_ID="$SESSION_ID" +SESSION_URL="$SESSION_URL" +SESSION_HOSTNAME="$SESSION_HOSTNAME" INGRESS_PROTOCOL="$INGRESS_PROTOCOL" INGRESS_DOMAIN="$INGRESS_DOMAIN" INGRESS_PORT_SUFFIX="$INGRESS_PORT_SUFFIX" @@ -259,7 +255,6 @@ IMAGE_REPOSITORY="$IMAGE_REPOSITORY" ASSETS_REPOSITORY="$ASSETS_REPOSITORY" SERVICES_PASSWORD="$SERVICES_PASSWORD" WORKSHOP_RENDERER="$WORKSHOP_RENDERER" -WORKSHOP_URL="$WORKSHOP_URL" EOF if [ x"$ENABLE_REGISTRY" == x"true" ]; then @@ -271,122 +266,10 @@ REGISTRY_SECRET="$REGISTRY_SECRET" EOF fi -# Run workshop specific setup scripts and then source the corresponding profile -# scripts as well so they are available for later setup scripts. Note that any -# errors in the profile scripts can cause the workshop container to not start. -# Since we cannot capture what happens in profile scripts into the log file, and -# because the container will not actually start anyway, it is necessary to look -# at the container log file instead. An error in setup scripts on the other -# hand shouldn't usually cause the container to not start and the log can be -# consulted from within the workshop session container. - -WORKSHOP_ENV=/tmp/workshop-env-$$.sh - -SETUP_LOGFILE=$HOME/.local/share/workshop/setup-scripts.log -SETUP_FAILED=$HOME/.local/share/workshop/setup-scripts.failed - -rm -f $SETUP_FAILED - -rm -f $SETUP_LOGFILE -touch $SETUP_LOGFILE - -function execute_setup_script() { - local script=$1 - echo "Executing: $script" - WORKSHOP_ENV=$WORKSHOP_ENV sh -x $script || touch $SETUP_FAILED || true - cat $WORKSHOP_ENV -} - -WORKSHOP_ENV_ROLLUP=$HOME/.local/share/workshop/workshop-env-$$.sh - -rm -f $WORKSHOP_ENV_ROLLUP -touch $WORKSHOP_ENV_ROLLUP - -for script in /opt/eduk8s/etc/setup.d/*.sh; do - if [ -x "$script" ]; then - truncate -s 0 $WORKSHOP_ENV - execute_setup_script $script 2>&1 | tee -a $SETUP_LOGFILE - set -a; . $WORKSHOP_ENV; set +a - cat $WORKSHOP_ENV >> $WORKSHOP_ENV_ROLLUP - [ -n "$(tail -c1 $WORKSHOP_ENV_ROLLUP)" ] && echo >> $WORKSHOP_ENV_ROLLUP - rm -f $WORKSHOP_ENV - fi -done - -mv $WORKSHOP_ENV_ROLLUP $HOME/.local/share/workshop/workshop-env-builtin.sh - -for script in /opt/eduk8s/etc/profile.d/*.sh /opt/eduk8s/etc/profile.d/sh.local; do - if [ -r "$script" ]; then - echo "Source: $script" - . "$script" - fi -done - -touch $WORKSHOP_ENV_ROLLUP - -for script in /opt/packages/*/setup.d/*.sh; do - if [ -x "$script" ]; then - truncate -s 0 $WORKSHOP_ENV - execute_setup_script $script 2>&1 | tee -a $SETUP_LOGFILE - set -a; . $WORKSHOP_ENV; set +a - cat $WORKSHOP_ENV >> $WORKSHOP_ENV_ROLLUP - [ -n "$(tail -c1 $WORKSHOP_ENV_ROLLUP)" ] && echo >> $WORKSHOP_ENV_ROLLUP - rm -f $WORKSHOP_ENV - fi -done - -mv $WORKSHOP_ENV_ROLLUP $HOME/.local/share/workshop/workshop-env-packages.sh +# Rebuild the workshop environment within the container, including generating +# any static workshop instructions if using the Hugo renderer. -for script in /opt/packages/*/profile.d/*.sh /opt/packages/*/profile.d/sh.local; do - if [ -r "$script" ]; then - echo "Source: $script" - . "$script" - fi -done - -touch $WORKSHOP_ENV_ROLLUP - -for script in /opt/workshop/setup.d/*.sh; do - if [ -x "$script" ]; then - truncate -s 0 $WORKSHOP_ENV - execute_setup_script $script 2>&1 | tee -a $SETUP_LOGFILE - set -a; . $WORKSHOP_ENV; set +a - cat $WORKSHOP_ENV >> $WORKSHOP_ENV_ROLLUP - [ -n "$(tail -c1 $WORKSHOP_ENV_ROLLUP)" ] && echo >> $WORKSHOP_ENV_ROLLUP - rm -f $WORKSHOP_ENV - fi -done - -mv $WORKSHOP_ENV_ROLLUP $HOME/.local/share/workshop/workshop-env-content.sh - -for script in /opt/workshop/profile.d/*.sh /opt/workshop/profile.d/sh.local; do - if [ -r "$script" ]; then - echo "Source: $script" - . "$script" - fi -done - -touch $WORKSHOP_ENV_ROLLUP - -for script in $HOME/workshop/setup.d/*.sh; do - if [ -x "$script" ]; then - truncate -s 0 $WORKSHOP_ENV - execute_setup_script $script 2>&1 | tee -a $SETUP_LOGFILE - set -a; . $WORKSHOP_ENV; set +a - cat $WORKSHOP_ENV >> $WORKSHOP_ENV_ROLLUP - [ -n "$(tail -c1 $WORKSHOP_ENV_ROLLUP)" ] && echo >> $WORKSHOP_ENV_ROLLUP - rm -f $WORKSHOP_ENV - fi -done - -mv $WORKSHOP_ENV_ROLLUP $HOME/.local/share/workshop/workshop-env-homedir.sh - -for script in $HOME/workshop/profile.d/*.sh $HOME/workshop/profile.d/sh.local; do - if [ -r "$script" ]; then - echo "Source: $script" - . "$script" - fi -done +. /opt/eduk8s/bin/rebuild-workshop # Run supervisord. See /opt/eduk8s/etc/supervisord.conf for the main # configuration. This is also symlinked to /etc/supervisord.conf so that @@ -405,7 +288,11 @@ ENABLE_CONSOLE_OCTANT_PROCESS=$ENABLE_CONSOLE_OCTANT ENABLE_WORKSHOP_PROCESS=$ENABLE_WORKSHOP if [ x"$ENABLE_WORKSHOP" == x"true" ]; then - if [ x"$WORKSHOP_RENDERER" != x"local" ]; then + if [ x"$WORKSHOP_RENDERER" == x"local" ]; then + if [ x"$LOCAL_RENDERER_TYPE" != x"classic" ]; then + ENABLE_WORKSHOP_PROCESS=false + fi + else ENABLE_WORKSHOP_PROCESS=false fi fi diff --git a/workshop-images/base-environment/opt/gateway/package-lock.json b/workshop-images/base-environment/opt/gateway/package-lock.json index f63a6a19..958b594d 100644 --- a/workshop-images/base-environment/opt/gateway/package-lock.json +++ b/workshop-images/base-environment/opt/gateway/package-lock.json @@ -39,8 +39,8 @@ "pug": "^3.0.2", "qrcode": "^1.5.1", "requirejs": "^2.3.6", - "semver": "^7.3.8", - "simple-oauth2": "^3.4.0", + "semver": "^7.5.3", + "simple-oauth2": "^5.0.0", "split.js": "^1.6.5", "utf-8-validate": "^5.0.10", "uuid": "^9.0.0", @@ -60,7 +60,7 @@ "@types/js-yaml": "^4.0.5", "@types/morgan": "^1.9.3", "@types/node": "^18.11.9", - "@types/simple-oauth2": "^2.5.3", + "@types/simple-oauth2": "^5.0.0", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "browserify": "^17.0.0", @@ -231,78 +231,80 @@ "node": ">=6" } }, - "node_modules/@hapi/address": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", - "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", - "deprecated": "Moved to 'npm install @sideway/address'" - }, "node_modules/@hapi/boom": { - "version": "7.4.11", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-7.4.11.tgz", - "integrity": "sha512-VSU/Cnj1DXouukYxxkes4nNJonCnlogHvIff1v1RVoN4xzkKhMXX+GRmb3NyH1iar10I9WFPDv2JPwfH3GaV0A==", - "deprecated": "This version has been deprecated and is no longer supported or maintained", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", "dependencies": { - "@hapi/hoek": "8.x.x" + "@hapi/hoek": "^11.0.2" } }, - "node_modules/@hapi/bourne": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", - "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", - "deprecated": "This version has been deprecated and is no longer supported or maintained" + "node_modules/@hapi/boom/node_modules/@hapi/hoek": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.2.tgz", + "integrity": "sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==" }, - "node_modules/@hapi/formula": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-1.2.0.tgz", - "integrity": "sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA==", - "deprecated": "Moved to 'npm install @sideway/formula'" + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==" }, "node_modules/@hapi/hoek": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", - "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", - "deprecated": "This version has been deprecated and is no longer supported or maintained" + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-10.0.1.tgz", + "integrity": "sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==" }, - "node_modules/@hapi/joi": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-16.1.8.tgz", - "integrity": "sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg==", - "deprecated": "Switch to 'npm install joi'", + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", "dependencies": { - "@hapi/address": "^2.1.2", - "@hapi/formula": "^1.2.0", - "@hapi/hoek": "^8.2.4", - "@hapi/pinpoint": "^1.0.2", - "@hapi/topo": "^3.1.3" + "@hapi/hoek": "^9.0.0" } }, - "node_modules/@hapi/pinpoint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-1.0.2.tgz", - "integrity": "sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ==", - "deprecated": "Moved to 'npm install @sideway/pinpoint'" + "node_modules/@hapi/topo/node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" }, - "node_modules/@hapi/topo": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", - "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", - "deprecated": "This version has been deprecated and is no longer supported or maintained", + "node_modules/@hapi/wreck": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.0.1.tgz", + "integrity": "sha512-OLHER70+rZxvDl75xq3xXOfd3e8XIvz8fWY0dqg92UvhZ29zo24vQgfqgHSYhB5ZiuFpSLeriOisAlxAo/1jWg==", "dependencies": { - "@hapi/hoek": "^8.3.0" + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/hoek": "^11.0.2" } }, - "node_modules/@hapi/wreck": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-15.1.0.tgz", - "integrity": "sha512-tQczYRTTeYBmvhsek/D49En/5khcShaBEmzrAaDjMrFXKJRuF8xA8+tlq1ETLBFwGd6Do6g2OC74rt11kzawzg==", - "deprecated": "This version has been deprecated and is no longer supported or maintained", + "node_modules/@hapi/wreck/node_modules/@hapi/hoek": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.2.tgz", + "integrity": "sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==" + }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", "dependencies": { - "@hapi/boom": "7.x.x", - "@hapi/bourne": "1.x.x", - "@hapi/hoek": "8.x.x" + "@hapi/hoek": "^9.0.0" } }, + "node_modules/@sideway/address/node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -456,9 +458,9 @@ } }, "node_modules/@types/simple-oauth2": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@types/simple-oauth2/-/simple-oauth2-2.5.5.tgz", - "integrity": "sha512-SLWEO4NCLNridfYIuZbuGI53MWVH7Sxkm2EQuaLuOW3FI/rpda/MDlQ4wTnVlZ0uLidEJE9tzAO2gNFkbHSzKA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/simple-oauth2/-/simple-oauth2-5.0.0.tgz", + "integrity": "sha512-rKipAaYvy2znetfn9AGC8GTfPJIwibSGCSzK75dibX0SPNdpfPjDQ4KarZq/k7ISF76MNZnaOtuFOpdKrNQyiA==", "dev": true }, "node_modules/@types/sizzle": { @@ -1365,18 +1367,6 @@ "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", "dev": true }, - "node_modules/date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2292,6 +2282,23 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "node_modules/joi": { + "version": "17.9.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.9.2.tgz", + "integrity": "sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==", + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/joi/node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, "node_modules/jquery": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz", @@ -3312,9 +3319,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -3442,16 +3449,14 @@ ] }, "node_modules/simple-oauth2": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-3.4.0.tgz", - "integrity": "sha512-gDPCC/xjq82FJnzF7+XGUUMWWfHeibuGsp3OYOV7yHwIibxHkq4+WSxywY63/3BF9j8SfIDygGqBrPLynx/iuQ==", - "deprecated": "simple-oauth2 v3 is no longer supported. Use simple-oauth2 v4 or higher for continued support", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz", + "integrity": "sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==", "dependencies": { - "@hapi/hoek": "^8.5.0", - "@hapi/joi": "^16.1.8", - "@hapi/wreck": "^15.1.0", - "date-fns": "^2.9.0", - "debug": "^4.1.1" + "@hapi/hoek": "^10.0.1", + "@hapi/wreck": "^18.0.0", + "debug": "^4.3.4", + "joi": "^17.6.4" } }, "node_modules/simple-oauth2/node_modules/debug": { @@ -4236,69 +4241,88 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==" }, - "@hapi/address": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", - "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==" - }, "@hapi/boom": { - "version": "7.4.11", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-7.4.11.tgz", - "integrity": "sha512-VSU/Cnj1DXouukYxxkes4nNJonCnlogHvIff1v1RVoN4xzkKhMXX+GRmb3NyH1iar10I9WFPDv2JPwfH3GaV0A==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", "requires": { - "@hapi/hoek": "8.x.x" + "@hapi/hoek": "^11.0.2" + }, + "dependencies": { + "@hapi/hoek": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.2.tgz", + "integrity": "sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==" + } } }, "@hapi/bourne": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", - "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==" - }, - "@hapi/formula": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-1.2.0.tgz", - "integrity": "sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==" }, "@hapi/hoek": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", - "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==" + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-10.0.1.tgz", + "integrity": "sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==" }, - "@hapi/joi": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-16.1.8.tgz", - "integrity": "sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg==", + "@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", "requires": { - "@hapi/address": "^2.1.2", - "@hapi/formula": "^1.2.0", - "@hapi/hoek": "^8.2.4", - "@hapi/pinpoint": "^1.0.2", - "@hapi/topo": "^3.1.3" + "@hapi/hoek": "^9.0.0" + }, + "dependencies": { + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + } } }, - "@hapi/pinpoint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-1.0.2.tgz", - "integrity": "sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ==" - }, - "@hapi/topo": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", - "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "@hapi/wreck": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.0.1.tgz", + "integrity": "sha512-OLHER70+rZxvDl75xq3xXOfd3e8XIvz8fWY0dqg92UvhZ29zo24vQgfqgHSYhB5ZiuFpSLeriOisAlxAo/1jWg==", "requires": { - "@hapi/hoek": "^8.3.0" + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/hoek": "^11.0.2" + }, + "dependencies": { + "@hapi/hoek": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.2.tgz", + "integrity": "sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==" + } } }, - "@hapi/wreck": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-15.1.0.tgz", - "integrity": "sha512-tQczYRTTeYBmvhsek/D49En/5khcShaBEmzrAaDjMrFXKJRuF8xA8+tlq1ETLBFwGd6Do6g2OC74rt11kzawzg==", + "@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", "requires": { - "@hapi/boom": "7.x.x", - "@hapi/bourne": "1.x.x", - "@hapi/hoek": "8.x.x" + "@hapi/hoek": "^9.0.0" + }, + "dependencies": { + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + } } }, + "@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -4452,9 +4476,9 @@ } }, "@types/simple-oauth2": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@types/simple-oauth2/-/simple-oauth2-2.5.5.tgz", - "integrity": "sha512-SLWEO4NCLNridfYIuZbuGI53MWVH7Sxkm2EQuaLuOW3FI/rpda/MDlQ4wTnVlZ0uLidEJE9tzAO2gNFkbHSzKA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/simple-oauth2/-/simple-oauth2-5.0.0.tgz", + "integrity": "sha512-rKipAaYvy2znetfn9AGC8GTfPJIwibSGCSzK75dibX0SPNdpfPjDQ4KarZq/k7ISF76MNZnaOtuFOpdKrNQyiA==", "dev": true }, "@types/sizzle": { @@ -5249,11 +5273,6 @@ "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", "dev": true }, - "date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -5950,6 +5969,25 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "joi": { + "version": "17.9.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.9.2.tgz", + "integrity": "sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==", + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + }, + "dependencies": { + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + } + } + }, "jquery": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz", @@ -6781,9 +6819,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "requires": { "lru-cache": "^6.0.0" } @@ -6878,15 +6916,14 @@ "dev": true }, "simple-oauth2": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-3.4.0.tgz", - "integrity": "sha512-gDPCC/xjq82FJnzF7+XGUUMWWfHeibuGsp3OYOV7yHwIibxHkq4+WSxywY63/3BF9j8SfIDygGqBrPLynx/iuQ==", - "requires": { - "@hapi/hoek": "^8.5.0", - "@hapi/joi": "^16.1.8", - "@hapi/wreck": "^15.1.0", - "date-fns": "^2.9.0", - "debug": "^4.1.1" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz", + "integrity": "sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==", + "requires": { + "@hapi/hoek": "^10.0.1", + "@hapi/wreck": "^18.0.0", + "debug": "^4.3.4", + "joi": "^17.6.4" }, "dependencies": { "debug": { diff --git a/workshop-images/base-environment/opt/gateway/package.json b/workshop-images/base-environment/opt/gateway/package.json index c886c3d3..31bd95cc 100644 --- a/workshop-images/base-environment/opt/gateway/package.json +++ b/workshop-images/base-environment/opt/gateway/package.json @@ -44,8 +44,8 @@ "pug": "^3.0.2", "qrcode": "^1.5.1", "requirejs": "^2.3.6", - "semver": "^7.3.8", - "simple-oauth2": "^3.4.0", + "semver": "^7.5.3", + "simple-oauth2": "^5.0.0", "split.js": "^1.6.5", "utf-8-validate": "^5.0.10", "uuid": "^9.0.0", @@ -65,7 +65,7 @@ "@types/js-yaml": "^4.0.5", "@types/morgan": "^1.9.3", "@types/node": "^18.11.9", - "@types/simple-oauth2": "^2.5.3", + "@types/simple-oauth2": "^5.0.0", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "browserify": "^17.0.0", diff --git a/workshop-images/base-environment/opt/gateway/src/backend/modules/access.ts b/workshop-images/base-environment/opt/gateway/src/backend/modules/access.ts index ff36e284..deb7fc2b 100644 --- a/workshop-images/base-environment/opt/gateway/src/backend/modules/access.ts +++ b/workshop-images/base-environment/opt/gateway/src/backend/modules/access.ts @@ -5,6 +5,8 @@ import * as path from "path" import * as https from "https" import { v4 as uuidv4 } from "uuid" +const { AuthorizationCode } = require('simple-oauth2') + const axios = require("axios").default import { logger } from "./logger" @@ -82,8 +84,8 @@ async function get_session_authorization(access_token: string) { return (await axios.get(url, options)).data } -async function verify_session_access(access_token: string) { - var details = await get_session_authorization(access_token) +async function verify_session_access(access_token: any) { + let details = await get_session_authorization(access_token["token"]["access_token"]) logger.info("Session details", details) @@ -95,7 +97,7 @@ async function verify_session_access(access_token: string) { let handshakes = {} -function register_oauth_callback(app: express.Application, oauth2: any, verify_user: any) { +function register_oauth_callback(app: express.Application, oauth2_config: any, oauth2_client: any, verify_user: any) { logger.info("Register OAuth callback.") app.get("/oauth_callback", async (req, res) => { @@ -121,30 +123,28 @@ function register_oauth_callback(app: express.Application, oauth2: any, verify_u let redirect_uri = [INGRESS_PROTOCOL, "://", req.get("host"), "/oauth_callback"].join("") - var options = { + let token_config = { redirect_uri: redirect_uri, scope: "user:info", code: code } - logger.debug("token_options", { options: options }) + logger.debug("token_config", { config: token_config }) - var auth_result = await oauth2.authorizationCode.getToken(options) - var token_result = oauth2.accessToken.create(auth_result) + let access_token = await oauth2_client.getToken(token_config) - logger.debug("auth_result", { result: auth_result }) - logger.debug("token_result", { result: token_result["token"] }) + logger.debug("access_token", { token: access_token }) // Now we need to verify whether this user is allowed access // to the project. - req.session.identity = await verify_user( - token_result["token"]["access_token"]) + req.session.identity = await verify_user(access_token) if (!req.session.identity) return res.status(403).json("Access forbidden") - req.session.token = token_result["token"]["access_token"] + req.session.token = JSON.stringify(access_token) + req.session.started = (new Date()).toISOString() logger.info("User access granted", req.session.identity) @@ -160,7 +160,7 @@ function register_oauth_callback(app: express.Application, oauth2: any, verify_u // Setup up redirection to the OAuth server authorization endpoint. -function register_oauth_handshake(app: express.Application, oauth2: any) { +function register_oauth_handshake(app: express.Application, oauth2_client: any) { logger.info("Register OAuth handshake") app.get("/oauth_handshake", (req, res) => { @@ -175,7 +175,7 @@ function register_oauth_handshake(app: express.Application, oauth2: any) { let redirect_uri = [INGRESS_PROTOCOL, "://", req.get("host"), "/oauth_callback"].join("") - const authorization_uri = oauth2.authorizationCode.authorizeURL({ + const authorization_uri = oauth2_client.authorizeURL({ redirect_uri: redirect_uri, scope: "user:info", state: state @@ -201,8 +201,8 @@ function register_oauth_handshake(app: express.Application, oauth2: any) { // training portal, which provides an OAuth provider endpoint for // authentication. -function setup_oauth_credentials(metadata: any, client_id: string, client_secret: string) { - var credentials = { +function setup_oauth_config(metadata: any, client_id: string, client_secret: string) { + let config = { client: { id: client_id, secret: client_secret @@ -220,7 +220,7 @@ function setup_oauth_credentials(metadata: any, client_id: string, client_secret } } - return credentials + return config } async function install_portal_auth(app: express.Application) { @@ -229,23 +229,25 @@ async function install_portal_auth(app: express.Application) { const client_id: string = PORTAL_CLIENT_ID const client_secret: string = PORTAL_CLIENT_SECRET - const metadata = { + const oauth2_metadata = { issuer: issuer, authorization_endpoint: issuer + "/oauth2/authorize/", token_endpoint: issuer + "/oauth2/token/" } - logger.info("OAuth server metadata", { metadata: metadata }) + logger.info("OAuth server metadata", { metadata: oauth2_metadata }) - const credentials = setup_oauth_credentials(metadata, client_id, + const oauth2_config = setup_oauth_config(oauth2_metadata, client_id, client_secret) - logger.info("OAuth server credentials", { credentials: credentials }) + const oauth2_client = new AuthorizationCode(oauth2_config) + + logger.info("OAuth server config", { oauth2_config: oauth2_config }) - const oauth2 = require('simple-oauth2').create(credentials) + register_oauth_callback(app, oauth2_config, oauth2_client, verify_session_access) + register_oauth_handshake(app, oauth2_client) - register_oauth_callback(app, oauth2, verify_session_access) - register_oauth_handshake(app, oauth2) + return oauth2_client } // Authentication via OAuth always has priority if configuration for HTTP @@ -255,11 +257,13 @@ async function install_portal_auth(app: express.Application) { // environment variables because of them being required parameters in a // template. -export async function setup_access(app: express.Application) { +export async function setup_access(app: express.Application): Promise { + let oauth2_client: any + if (PORTAL_CLIENT_ID) { logger.info("Install portal oauth support.") - await install_portal_auth(app) + oauth2_client = await install_portal_auth(app) } else if (AUTH_USERNAME) { if (AUTH_USERNAME != "*") { @@ -271,4 +275,39 @@ export async function setup_access(app: express.Application) { logger.info("All authentication has been disabled.") } } + + return oauth2_client +} + +// Helper function for checking whether access token is going to expire and +// request a new one using the refresh token. If we fail to refresh the token +// we just log it and return without failing. This will result in higher +// level function needing the access token to fail instead. + +const EXPIRATION_WINDOW_IN_SECONDS = 15*60 + +export async function check_for_access_token_expiry(session: any, oauth2_client: any) { + let access_token = oauth2_client.createToken(JSON.parse(session.token)) + + function expiring() : boolean { + return access_token.token.expires_at - (Date.now() + EXPIRATION_WINDOW_IN_SECONDS * 1000) <= 0 + } + + if (expiring()) { + try { + logger.debug("Refreshing accessing token", {token: access_token}) + + let refresh_params = { + scope: "user:info" + } + + access_token = await access_token.refresh(refresh_params) + + logger.debug("Refreshed access token", {token: access_token}) + + session.token = JSON.stringify(access_token) + } catch (error) { + logger.error("Error refreshing access token", { message: error.message }) + } + } } diff --git a/workshop-images/base-environment/opt/gateway/src/backend/modules/config.ts b/workshop-images/base-environment/opt/gateway/src/backend/modules/config.ts index a15fe87a..b6cfe647 100644 --- a/workshop-images/base-environment/opt/gateway/src/backend/modules/config.ts +++ b/workshop-images/base-environment/opt/gateway/src/backend/modules/config.ts @@ -1,7 +1,6 @@ import * as os from "os" import * as fs from "fs" import * as path from "path" -import * as yaml from "js-yaml" const PLATFORM_ARCH = process.env.PLATFORM_ARCH || "" @@ -10,7 +9,12 @@ const TRAINING_PORTAL = process.env.TRAINING_PORTAL || "workshop" const ENVIRONMENT_NAME = process.env.ENVIRONMENT_NAME || "workshop" const WORKSHOP_NAMESPACE = process.env.WORKSHOP_NAMESPACE || "workshop" const SESSION_NAMESPACE = process.env.SESSION_NAMESPACE || "workshop" +const SESSION_NAME = process.env.SESSION_NAME || "workshop" const SESSION_ID = process.env.SESSION_ID || "workshop" +const SESSION_URL = process.env.SESSION_URL || "http://workshop-127-0-0-1.nip.io" +const SESSION_HOSTNAME = process.env.SESSION_HOSTNAME || "workshop-127-0-0-1.nip.io" + +const CLUSTER_DOMAIN = process.env.CLUSTER_DOMAIN || "cluster.local" const INGRESS_PROTOCOL = process.env.INGRESS_PROTOCOL || "http" const INGRESS_DOMAIN = process.env.INGRESS_DOMAIN || "127-0-0-1.nip.io" @@ -49,7 +53,7 @@ const WEBDAV_PORT = process.env.WEBDAV_PORT const WORKSHOP_PORT = process.env.WORKSHOP_PORT const WORKSHOP_RENDERER = process.env.WORKSHOP_RENDERER -const WORKSHOP_URL = process.env.WORKSHOP_URL +const LOCAL_RENDERER_TYPE = process.env.LOCAL_RENDERER_TYPE const WORKSHOP_DIR = process.env.WORKSHOP_DIR const SLIDES_DIR = process.env.SLIDES_DIR @@ -62,10 +66,12 @@ const TERMINAL_LAYOUT = process.env.TERMINAL_LAYOUT || "default" const RESTART_URL = process.env.RESTART_URL const FINISHED_MSG = process.env.FINISHED_MSG -const IMAGE_REPOSITORY = process.env.IMAGE_REPOSITORY || "registry.default.svc.cluster.local:5001" +const IMAGE_REPOSITORY = process.env.IMAGE_REPOSITORY || "registry.default.svc.cluster.local" +const OCI_IMAGE_CACHE = process.env.OCI_IMAGE_CACHE || "workshop-images" const ASSETS_REPOSITORY = process.env.ASSETS_REPOSITORY || "workshop-assets" const SERVICES_PASSWORD = process.env.SERVICES_PASSWORD +const CONFIG_PASSWORD = process.env.CONFIG_PASSWORD function kubernetes_token() { if (fs.existsSync("/var/run/secrets/kubernetes.io/serviceaccount/token")) @@ -73,14 +79,14 @@ function kubernetes_token() { } function load_workshop() { - let config_pathname = path.join(os.homedir(), ".local/share/workshop/workshop-definition.yaml") + let config_pathname = path.join(os.homedir(), ".local/share/workshop/workshop-definition.json") if (!fs.existsSync(config_pathname)) return {} let config_contents = fs.readFileSync(config_pathname, "utf8") - return yaml.load(config_contents) + return JSON.parse(config_contents) } export let config = { @@ -93,8 +99,13 @@ export let config = { environment_name: ENVIRONMENT_NAME, workshop_namespace: WORKSHOP_NAMESPACE, session_namespace: SESSION_NAMESPACE, + session_name: SESSION_NAME, session_id: SESSION_ID, + session_url: SESSION_URL, + + cluster_domain: CLUSTER_DOMAIN, + session_hostname: SESSION_HOSTNAME, ingress_protocol: INGRESS_PROTOCOL, ingress_domain: INGRESS_DOMAIN, ingress_port_suffix: INGRESS_PORT_SUFFIX, @@ -140,7 +151,11 @@ export let config = { workshop_port: WORKSHOP_PORT, workshop_renderer: WORKSHOP_RENDERER, - workshop_url: WORKSHOP_URL, + local_renderer_type: LOCAL_RENDERER_TYPE, + + workshop_url: "", + workshop_proxy: {}, + workshop_path: "", restart_url: RESTART_URL, finished_msg: FINISHED_MSG, @@ -148,28 +163,38 @@ export let config = { kubernetes_token: kubernetes_token(), image_repository: IMAGE_REPOSITORY, + oci_image_cache: OCI_IMAGE_CACHE, assets_repository: ASSETS_REPOSITORY, services_password: SERVICES_PASSWORD, + config_password: CONFIG_PASSWORD, dashboards: [], ingresses: [], + + dashboard_tabs: "", } -function substitute_session_params(value: string) { +function substitute_session_params(value: any) { if (!value) return value value = value.split("$(platform_arch)").join(config.platform_arch) value = value.split("$(image_repository)").join(config.image_repository) + value = value.split("$(oci_image_cache)").join(config.oci_image_cache) value = value.split("$(assets_repository)").join(config.assets_repository) value = value.split("$(environment_name)").join(config.environment_name) value = value.split("$(workshop_namespace)").join(config.workshop_namespace) value = value.split("$(session_namespace)").join(config.session_namespace) + value = value.split("$(session_name)").join(config.session_name) value = value.split("$(session_id)").join(config.session_id) + value = value.split("$(session_hostname)").join(config.session_hostname) + value = value.split("$(cluster_domain)").join(config.cluster_domain) value = value.split("$(ingress_domain)").join(config.ingress_domain) value = value.split("$(ingress_protocol)").join(config.ingress_protocol) value = value.split("$(ingress_port_suffix)").join(config.ingress_port_suffix) + value = value.split("$(services_password)").join(config.services_password) + value = value.split("$(config_password)").join(config.config_password) return value } @@ -186,33 +211,136 @@ function string_to_slug(str: string) { .replace(/-+$/, "") // trim - from end of text } +function lookup_application(name) { + let workshop_spec = config.workshop["spec"] + + if (!workshop_spec) + return {} + + let workshop_session = config.workshop["spec"]["session"] + + if (!workshop_session) + return {} + + let applications = workshop_session["applications"] + + if (!applications) + return {} + + let application = applications[name] + + if (!application) + return {} + + return application +} + +function calculate_workshop_url() { + let application = lookup_application("workshop") + + return substitute_session_params(application["url"]) +} + +function calculate_workshop_proxy() { + let application = lookup_application("workshop") + + if (!application) + return config.workshop_proxy + + let proxy_details = application["proxy"] + + if (!proxy_details) + return config.workshop_proxy + + let protocol = substitute_session_params(proxy_details["protocol"]) || "http" + let host = substitute_session_params(proxy_details["host"]) + let port = proxy_details["port"] + let headers = proxy_details["headers"] || [] + let rewrite_rules = proxy_details["pathRewrite"] || [] + + let change_origin = proxy_details["changeOrigin"] + + if (change_origin === undefined) + change_origin = true + + if (!port || port == "0") + port = protocol == "https" ? 443 : 80 + + let expanded_headers = [] + + for (let item of headers) { + expanded_headers.push({ + name: item["name"], + value: substitute_session_params(item["value"] || "") + }) + } + + return { + protocol: protocol, + host: host, + port: port, + headers: expanded_headers, + changeOrigin: change_origin, + pathRewrite: rewrite_rules, + } +} + +function calculate_workshop_path() { + let application = lookup_application("workshop") + + let workshop_path = substitute_session_params(application["path"]) + + if (!workshop_path) + return path.join(config.workshop_dir, "public") + + if (path.isAbsolute(workshop_path)) + return workshop_path + + return path.join(config.workshop_path, workshop_path) +} + function calculate_dashboards() { let all_dashboards = [] + let builtin_dashboards = {} + let builtin_dashboard_tabs = [] + + if (config.enable_terminal) { + builtin_dashboard_tabs.push("terminal") + builtin_dashboards["terminal"] = { + "id": "terminal", + "name": "Terminal" + } + } if (config.enable_console && config.console_url) { - all_dashboards.push({ + builtin_dashboard_tabs.push("console") + builtin_dashboards["console"] = { "id": "console", "name": "Console", "url": config.console_url - }) + } } if (config.enable_editor && config.editor_url) { - all_dashboards.push({ + builtin_dashboard_tabs.push("editor") + builtin_dashboards["editor"] = { "id": "editor", "name": "Editor", "url": config.editor_url - }) + } } if (config.enable_slides && config.slides_url) { - all_dashboards.push({ + builtin_dashboard_tabs.push("slides") + builtin_dashboards["slides"] = { "id": "slides", "name": "Slides", "url": config.slides_url - }) + } } + let builtins_count = Object.keys(builtin_dashboards).length + let workshop_spec = config.workshop["spec"] if (!workshop_spec) @@ -226,7 +354,7 @@ function calculate_dashboards() { if (dashboards) { for (let i = 0; i < dashboards.length; i++) { if (dashboards[i]["name"]) { - let url: string = dashboards[i]["url"] + let url: string = dashboards[i]["url"] || "" let terminal: string = null url = substitute_session_params(url) @@ -236,20 +364,59 @@ function calculate_dashboards() { url = null } - all_dashboards.push({ - "id": string_to_slug(dashboards[i]["name"]), - "name": dashboards[i]["name"], - "terminal": terminal, - "url": url - }) + let id = string_to_slug(dashboards[i]["name"]) + + if (id in builtin_dashboards) { + all_dashboards.push(builtin_dashboards[id]) + delete builtin_dashboards[id] + } + else { + all_dashboards.push({ + "id": id, + "name": dashboards[i]["name"], + "terminal": terminal, + "url": url + }) + } } } } } + if (builtins_count == Object.keys(builtin_dashboards).length) { + let dashboard_items = [] + for (let i = 0; i < builtin_dashboard_tabs.length; i++) { + let name = builtin_dashboard_tabs[i] + if (name in builtin_dashboards) { + let value = builtin_dashboards[name] + dashboard_items.push(value) + } + } + all_dashboards = [...dashboard_items, ...all_dashboards] + } + else { + for (let i = 0; i < builtin_dashboard_tabs.length; i++) { + let name = builtin_dashboard_tabs[i] + if (name in builtin_dashboards) { + let value = builtin_dashboards[name] + all_dashboards.push(value) + } + } + } + return all_dashboards } +function calculate_dashboard_tabs(dashboards) { + let names = [] + + for (let i = 0; i < dashboards.length; i++) { + names.push(dashboards[i]["id"]) + } + + return names.join(",") +} + function calculate_ingresses() { let all_ingresses = [] @@ -288,7 +455,11 @@ function calculate_ingresses() { return all_ingresses } +config.workshop_url = calculate_workshop_url() +config.workshop_proxy = calculate_workshop_proxy() +config.workshop_path = calculate_workshop_path() + config.dashboards = calculate_dashboards() config.ingresses = calculate_ingresses() -config.workshop_url = substitute_session_params(config.workshop_url) +config.dashboard_tabs = calculate_dashboard_tabs(config.dashboards) diff --git a/workshop-images/base-environment/opt/gateway/src/backend/modules/dashboard.ts b/workshop-images/base-environment/opt/gateway/src/backend/modules/dashboard.ts index 9dc7641a..6c8aeb59 100644 --- a/workshop-images/base-environment/opt/gateway/src/backend/modules/dashboard.ts +++ b/workshop-images/base-environment/opt/gateway/src/backend/modules/dashboard.ts @@ -35,7 +35,7 @@ function load_finished_html() { return fs.readFileSync(html_pathname, "utf8") } -export function setup_dashboard(app: express.Application) { +export function setup_dashboard(app: express.Application, oauth2_client: any) { if (!config.enable_dashboard) return @@ -56,7 +56,7 @@ export function setup_dashboard(app: express.Application) { let data = { error: "setup-scripts-failed" } try { - await send_analytics_event(req.session.token, "Workshop/Error", { data: data }) + await send_analytics_event(req.session, oauth2_client, "Workshop/Error", { data: data }) } catch (err) { // Ignore any error as we don't want it prevent page loading. } @@ -71,7 +71,7 @@ export function setup_dashboard(app: express.Application) { let data = { error: "download-workshop-failed" } try { - await send_analytics_event(req.session.token, "Workshop/Error", { data: data }) + await send_analytics_event(req.session, oauth2_client, "Workshop/Error", { data: data }) } catch (err) { // Ignore any error as we don't want it prevent page loading. } diff --git a/workshop-images/base-environment/opt/gateway/src/backend/modules/routing.ts b/workshop-images/base-environment/opt/gateway/src/backend/modules/routing.ts index 8fd99ad1..0b279c1e 100644 --- a/workshop-images/base-environment/opt/gateway/src/backend/modules/routing.ts +++ b/workshop-images/base-environment/opt/gateway/src/backend/modules/routing.ts @@ -15,7 +15,7 @@ export function setup_routing(app: express.Application) { routes_directories.push(path.join(BASEDIR, "build/backend/routes")) routes_directories.push("/opt/eduk8s/etc/gateway/routes") - routes_directories.push(...glob.sync("/opt/packages/*/etc/gateway/routes")) + routes_directories.push(...glob.sync("/opt/packages/*/gateway/routes")) routes_directories.push("/opt/workshop/gateway/routes") routes_directories.push("/home/eduk8s/workshop/gateway/routes") diff --git a/workshop-images/base-environment/opt/gateway/src/backend/modules/session.ts b/workshop-images/base-environment/opt/gateway/src/backend/modules/session.ts index 1d795ba2..0d5dd6cb 100644 --- a/workshop-images/base-environment/opt/gateway/src/backend/modules/session.ts +++ b/workshop-images/base-environment/opt/gateway/src/backend/modules/session.ts @@ -2,60 +2,97 @@ import * as express from "express" const axios = require("axios").default +import { check_for_access_token_expiry } from "./access" + import { logger } from "./logger" + const PORTAL_API_URL = process.env.PORTAL_API_URL const SESSION_NAME = process.env.SESSION_NAME -async function get_session_schedule(access_token) { +async function get_session_schedule(session, oauth2_client) { + await check_for_access_token_expiry(session, oauth2_client) + + let access_token = oauth2_client.createToken(JSON.parse(session.token)) + const options = { baseURL: PORTAL_API_URL, - headers: { "Authorization": "Bearer " + access_token }, + headers: { "Authorization": "Bearer " + access_token["token"]["access_token"] }, responseType: "json" } const url = "/workshops/session/" + SESSION_NAME + "/schedule/" - return (await axios.get(url, options)).data + try { + return (await axios.get(url, options)).data + } catch (error) { + logger.error("Error retrieving session schedule", { status: error.response.status, data: error.response.data }) + + throw new Error("Error retrieving session schedule") + } } -async function get_extend_schedule(access_token) { +async function get_extend_schedule(session, oauth2_client) { + await check_for_access_token_expiry(session, oauth2_client) + + let access_token = oauth2_client.createToken(JSON.parse(session.token)) + const options = { baseURL: PORTAL_API_URL, - headers: { "Authorization": "Bearer " + access_token }, + headers: { "Authorization": "Bearer " + access_token["token"]["access_token"] }, responseType: "json" } const url = "/workshops/session/" + SESSION_NAME + "/extend/" - return (await axios.get(url, options)).data + try { + return (await axios.get(url, options)).data + } catch (error) { + logger.error("Error extending session duration", { status: error.response.status, data: error.response.data }) + + throw new Error("Error extending session duration") + } } -export async function send_analytics_event(access_token, event, data) { +export async function send_analytics_event(session, oauth2_client, event, data) { + await check_for_access_token_expiry(session, oauth2_client) + + let access_token = oauth2_client.createToken(JSON.parse(session.token)) + const options = { baseURL: PORTAL_API_URL, - headers: { "Authorization": "Bearer " + access_token }, + headers: { "Authorization": "Bearer " + access_token["token"]["access_token"] }, responseType: "json" } const url = "/workshops/session/" + SESSION_NAME + "/event/" - let payload = {"event": {}} + let payload = { "event": {} } + + Object.assign(payload["event"], data, { "name": event }) - Object.assign(payload["event"], data, {"name": event}) + try { + return (await axios.post(url, payload, options)).data + } catch (error) { + logger.error("Error reporting workshop event", error.response.status, error.response.data) - return (await axios.post(url, payload, options)).data + throw new Error("Error reporting workshop event") + } } -export function setup_session(app: express.Application) { +export function setup_session(app: express.Application, oauth2_client: any) { app.get("/session/schedule", async (req, res) => { if (req.session.token) { - let details = await get_session_schedule(req.session.token) + try { + let details = await get_session_schedule(req.session, oauth2_client) - logger.info("Session schedule", details) + logger.info("Session schedule", details) - return res.json(details) + return res.json(details) + } catch (error) { + return res.status(500).send(error.message) + } } res.json({}) @@ -63,11 +100,15 @@ export function setup_session(app: express.Application) { app.get("/session/extend", async (req, res) => { if (req.session.token) { - let details = await get_extend_schedule(req.session.token) + try { + let details = await get_extend_schedule(req.session, oauth2_client) - logger.info("Extended schedule", details) + logger.info("Extended schedule", details) - return res.json(details) + return res.json(details) + } catch (error) { + return res.status(500).send(error.message) + } } res.json({}) @@ -77,18 +118,22 @@ export function setup_session(app: express.Application) { app.post("/session/event", async (req, res) => { if (req.session.token) { - let payload = req.body + try { + let payload = req.body - let data = payload["event"] - let event = data["name"] + let data = payload["event"] + let event = data["name"] - logger.info("Forwarding event", payload) + logger.info("Forwarding event", payload) - delete data["name"] + delete data["name"] - let details = await send_analytics_event(req.session.token, event, data) + let details = await send_analytics_event(req.session, oauth2_client, event, data) - return res.json(details) + return res.json(details) + } catch (error) { + return res.status(500).send(error.message) + } } res.json({}) diff --git a/workshop-images/base-environment/opt/gateway/src/backend/modules/workshop.ts b/workshop-images/base-environment/opt/gateway/src/backend/modules/workshop.ts index 2f7575b1..5aa70a3d 100644 --- a/workshop-images/base-environment/opt/gateway/src/backend/modules/workshop.ts +++ b/workshop-images/base-environment/opt/gateway/src/backend/modules/workshop.ts @@ -8,27 +8,39 @@ let axios_retry = require("axios-retry") import { config } from "./config" -const URL_REGEX = new RegExp(/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi) - export function setup_workshop(app: express.Application) { let workshop_url = config.workshop_url || '/workshop/' app.get('/workshop/.redirect-when-workshop-is-ready', function (req, res) { - // If a workshop URL is provided which maps to a qualified http/https - // URL, assume that is externally hosted workshop content and perform - // an immediate redirect to that site. + // If renderer is declared as being remote, in which case a workshop URL + // should be provided which maps to a qualified http/https URL, assume + // that is really externally hosted workshop content and perform an + // immediate redirect to that site. if (config.workshop_renderer == "remote") return res.redirect(workshop_url) - // If workshop content renderer isn't enabled then redirect as well. + // In the case of having static workshop content, then also redirect. + // In this case it should normally redirect to /workshop/ although + // technically could redirect to a different sub URL path. + + if (config.workshop_renderer == "static") + return res.redirect(workshop_url) + + // Finally, if workshop content renderer isn't enabled then redirect as + // well. This should probably end up with an error when redirected. if (!config.enable_workshop) return res.redirect(workshop_url) // Check whether the internal workshop content renderer is ready. - var client = axios.create({ baseURL: 'http://127.0.0.1:' + config.workshop_port }) + let url = 'http://127.0.0.1:' + config.workshop_port + + if (config.workshop_renderer == "proxy") + url = workshop_url + + var client = axios.create({ baseURL: url }) var options = { retries: 3, @@ -39,13 +51,18 @@ export function setup_workshop(app: express.Application) { axios_retry(client, options) - client.get('/workshop/') + let redirect_url = workshop_url + + if (config.workshop_renderer == "proxy") + redirect_url = '/workshop/content/' + + client.get('/workshop/content/') .then((result) => { - res.redirect(workshop_url) + res.redirect(redirect_url) }) .catch((error) => { console.log('Error with workshop backend', error) - res.redirect(workshop_url) + res.redirect(redirect_url) }) }) @@ -58,13 +75,127 @@ export function setup_workshop(app: express.Application) { res.redirect(workshop_url) }) } - else if (config.workshop_renderer == "static") { - app.use("/workshop/", express.static(path.join(config.workshop_dir, "public"))) - } - else { + else if (config.workshop_renderer == "local" && config.local_renderer_type == "classic") { app.use(createProxyMiddleware("/workshop/", { target: 'http://127.0.0.1:' + config.workshop_port, ws: true, + onError: (err, req, res) => { + // The error handler can be called for either HTTP requests + // or a web socket connection. Check whether have writeHead + // method, indicating it is a HTTP request. Otherwise it is + // actually a socket object and shouldn't do anything. + + console.log("Proxy", err) + + if (res.writeHead) + res.status(503).render("proxy-error-page") + } + })) + } + else if (config.workshop_renderer == "proxy") { + app.get("/workshop/$", (req, res) => { + res.redirect('/workshop/content/') + }) + + let protocol = config.workshop_proxy["protocol"] + let host = config.workshop_proxy["host"] + let port = config.workshop_proxy["port"] + let headers = config.workshop_proxy["headers"] + let change_origin = config.workshop_proxy["changeOrigin"] + let path_rewrite = config.workshop_proxy["pathRewrite"] + + let path_rewrite_map = {} + + for (let item of path_rewrite) { + path_rewrite_map[item["pattern"]] = item["replacement"] + } + + let target = `${protocol}://${host}:${port}` + + app.use(createProxyMiddleware("/workshop/content/", { + target: target, + changeOrigin: change_origin, + pathRewrite: path_rewrite_map, + ws: true, + onProxyReq: (proxyReq, req, res) => { + for (let i = 0; i < headers.length; i++) { + let header = headers[i] + let name = header["name"] + let value = header["value"] || "" + proxyReq.setHeader(name, value) + } + }, + onProxyReqWs: (proxyReq, req, socket, options, head) => { + for (let i = 0; i < headers.length; i++) { + let header = headers[i] + let name = header["name"] + let value = header["value"] || "" + proxyReq.setHeader(name, value) + } + }, + onProxyRes: (proxyRes, req, res) => { + delete proxyRes.headers["x-frame-options"] + delete proxyRes.headers["content-security-policy"] + res.append("Access-Control-Allow-Origin", ["*"]) + res.append("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,HEAD") + res.append("Access-Control-Allow-Headers", "Content-Type") + }, + onError: (err, req, res) => { + // The error handler can be called for either HTTP requests + // or a web socket connection. Check whether have writeHead + // method, indicating it is a HTTP request. Otherwise it is + // actually a socket object and shouldn't do anything. + + console.log("Proxy", err) + + if (res.writeHead) + res.status(503).render("proxy-error-page") + } })) } + else { + // In the case of static workshop content the requirement is that it + // be at a base URL with sub path of /workshop/content/ so redirect + // to that. This is so there is no conflict with /workshop/static. + + app.get("/workshop/$", (req, res) => { + res.redirect('/workshop/content/') + }) + + app.use("/workshop/content/", express.static(path.join(config.workshop_dir, "public"))) + } +} + +// We expose select URLs for accessing workshop configuration. + +export function setup_workshop_config(app: express.Application, token: string = null) { + function handler(filename) { + return express.static(filename) + } + + if (token) { + function auth_handler(filename) { + return async function (req, res, next) { + let request_token = req.query.token + + if (!request_token || request_token != token) + return next() + + return await handler(filename)(req, res, next) + } + } + + app.use("/config/environment", auth_handler("/home/eduk8s/.local/share/workshop/workshop-environment.json")) + app.use("/config/variables", auth_handler("/home/eduk8s/.local/share/workshop/workshop-variables.json")) + app.use("/config/kubeconfig", auth_handler("/home/eduk8s/.kube/config")) + app.use("/config/id_rsa", auth_handler("/home/eduk8s/.ssh/id_rsa")) + app.use("/config/id_rsa.pub", auth_handler("/home/eduk8s/.ssh/id_rsa.pub")) + } + else { + app.use("/config/environment", handler("/home/eduk8s/.local/share/workshop/workshop-environment.json")) + app.use("/config/variables", handler("/home/eduk8s/.local/share/workshop/workshop-variables.json")) + app.use("/config/kubeconfig", handler("/home/eduk8s/.kube/config")) + app.use("/config/id_rsa", handler("/home/eduk8s/.ssh/id_rsa")) + app.use("/config/id_rsa.pub", handler("/home/eduk8s/.ssh/id_rsa.pub")) + } } diff --git a/workshop-images/base-environment/opt/gateway/src/backend/server.ts b/workshop-images/base-environment/opt/gateway/src/backend/server.ts index 9bc0cb66..5043ef60 100644 --- a/workshop-images/base-environment/opt/gateway/src/backend/server.ts +++ b/workshop-images/base-environment/opt/gateway/src/backend/server.ts @@ -16,7 +16,7 @@ import { setup_dashboard } from "./modules/dashboard" import { setup_assets } from "./modules/assets" import { setup_slides } from "./modules/slides" import { setup_examiner } from "./modules/examiner" -import { setup_workshop } from "./modules/workshop" +import { setup_workshop, setup_workshop_config } from "./modules/workshop" import { setup_files } from "./modules/files" import { setup_uploads } from "./modules/uploads" import { setup_routing } from "./modules/routing" @@ -88,11 +88,21 @@ setup_proxy(app, "none") // secure cookie. If the ingress protocol wasn't actually "http", this means // that access to the workshop session will be blocked. -const INGRESS_PROTOCOL = process.env.INGRESS_PROTOCOL || "http" +const ENVIRONMENT_NAME = process.env.ENVIRONMENT_NAME || "workshop" + const FRAME_ANCESTORS = process.env.FRAME_ANCESTORS +const SESSION_COOKIE_DOMAIN = process.env.SESSION_COOKIE_DOMAIN || null + +var cookie_name = "workshop-session-id" + +if (SESSION_COOKIE_DOMAIN) { + cookie_name = `sessionid-${ENVIRONMENT_NAME}` +} + let cookie_options: express.CookieOptions = { path: "/", + domain: SESSION_COOKIE_DOMAIN, secure: false, sameSite: "lax", maxAge: 24 * 60 * 60 * 1000 @@ -104,7 +114,7 @@ if (FRAME_ANCESTORS) { } app.use(session({ - name: "workshop-session-id", + name: cookie_name, genid: (req) => { return uuidv4() }, secret: uuidv4(), cookie: cookie_options, @@ -138,8 +148,12 @@ function start_http_server() { async function main() { try { + let oauth2_client: any + setup_signals() + setup_workshop_config(app, config.config_password) + setup_files(app, config.services_password) setup_uploads(app, config.services_password) @@ -151,17 +165,21 @@ async function main() { setup_assets(app) - await setup_access(app) + oauth2_client = await setup_access(app) setup_proxy(app, "session") - setup_session(app) + + setup_session(app, oauth2_client) + + setup_workshop_config(app) + setup_terminals(app, server) setup_workshop(app) setup_slides(app) setup_examiner(app) setup_files(app) setup_uploads(app) - setup_dashboard(app) + setup_dashboard(app, oauth2_client) setup_routing(app) diff --git a/workshop-images/base-environment/opt/gateway/src/backend/views/dashboard-page.pug b/workshop-images/base-environment/opt/gateway/src/backend/views/dashboard-page.pug index 1c0e4513..95289f87 100644 --- a/workshop-images/base-environment/opt/gateway/src/backend/views/dashboard-page.pug +++ b/workshop-images/base-environment/opt/gateway/src/backend/views/dashboard-page.pug @@ -31,6 +31,7 @@ html.no-scrolling data-ingress-domain=config.ingress_domain, data-ingress-protocol=config.ingress_protocol, data-ingress-port-suffix=config.ingress_port_suffix, + data-dashboard-tabs=config.dashboard_tabs, data-workshop-layout=config.workshop_layout, data-terminal-layout=config.terminal_layout, data-workshop-url=config.workshop_url, diff --git a/workshop-images/base-environment/opt/gateway/src/frontend/scripts/educates.ts b/workshop-images/base-environment/opt/gateway/src/frontend/scripts/educates.ts index 2b08fc8b..7bd02460 100644 --- a/workshop-images/base-environment/opt/gateway/src/frontend/scripts/educates.ts +++ b/workshop-images/base-environment/opt/gateway/src/frontend/scripts/educates.ts @@ -876,6 +876,8 @@ class Dashboard { private expiring: boolean constructor() { + let $body = $("body") + if ($("#dashboard").length) { // To indicate progress, update message on startup cover panel. Also // hide the cover panel after 15 seconds if we don't get through all @@ -958,6 +960,46 @@ class Dashboard { }) } + // Reorder the dashboard tabs and ensure the first tab is the one that + // is displayed and has focus. + + if ($body.data("dashboard-tabs")) { + let tabs = $body.data("dashboard-tabs").split(",") + + let tab_ids = [] + + for (let id in tabs) { + tab_ids.push(`${tabs[id]}-tab`) + } + + let navbar = $("#workarea-nav") + + let matched_tabs = {} + + let remaining_tabs = [] + + $('#workarea-nav > li').each((_, element) => { + let id = $(element).children().first().attr("id") + if (tab_ids.includes(id)) { + matched_tabs[id] = element + } else { + remaining_tabs.push(element) + } + }) + + navbar.empty() + + tab_ids.forEach(name => { + if (name in matched_tabs) { + navbar.append(matched_tabs[name]) + } + }) + + remaining_tabs.forEach(element => navbar.append(element)) + + $(`#${tabs[0]}`).trigger("click") + } + // Add a click action to any panel with a child iframe set up for // delayed loading when first click performed. @@ -1318,6 +1360,10 @@ class Dashboard { $("#finished-workshop-dialog").modal("show") } + terminate_session() { + $("#terminate-session-dialog").modal("show") + } + verify_origin(origin: string): boolean { if (origin == window.origin) return true @@ -1357,11 +1403,16 @@ class Dashboard { $("#preview-image-dialog").modal("show") } - reload_dashboard(name: string, url?: string): boolean { + reload_dashboard(name: string, url: string = "", focus: boolean = true): boolean { let id = string_to_slug(name) - if (!this.expose_dashboard(id)) - return false + let tab_anchor = $(`#${id}-tab`) + + if (!tab_anchor.length) + return this.create_dashboard(name, url, focus) + + if (focus) + tab_anchor.trigger("click") if (name != "terminal") { let tab = $(`#${id}-tab`) @@ -1415,7 +1466,7 @@ class Dashboard { return true } - create_dashboard(name: string, url: string): boolean { + create_dashboard(name: string, url: string, focus: boolean = true): boolean { if (!name) return @@ -1479,7 +1530,8 @@ class Dashboard { // Now trigger click action on the tab to expose new dashboard tab. - tab_anchor.trigger("click") + if (focus) + tab_anchor.trigger("click") return true } @@ -1563,6 +1615,12 @@ interface DashboardSelectOptions { interface DashboardCreateOptions { name: string url: string + focus: boolean +} + +interface DashboardPreviewOptions { + src: string + title: string } const action_table = { @@ -1619,13 +1677,22 @@ const action_table = { dashboard.expose_dashboard(args.name) }, "dashboard:create-dashboard": function (args: DashboardCreateOptions) { - dashboard.create_dashboard(args.name, args.url) + dashboard.create_dashboard(args.name, args.url, args.focus) }, "dashboard:delete-dashboard": function (args: DashboardSelectOptions) { dashboard.delete_dashboard(args.name) }, "dashboard:reload-dashboard": function (args: DashboardCreateOptions) { - dashboard.reload_dashboard(args.name, args.url) + dashboard.reload_dashboard(args.name, args.url, args.focus) + }, + "dashboard:preview-image": function (args: DashboardPreviewOptions) { + dashboard.preview_image(args.src, args.title) + }, + "dashboard:finished-workshop": function () { + dashboard.finished_workshop() + }, + "dashboard:terminate-session": function () { + dashboard.terminate_session() }, } diff --git a/workshop-images/base-environment/opt/gateway/src/types/index.d.ts b/workshop-images/base-environment/opt/gateway/src/types/index.d.ts index 7e38339e..ad241011 100644 --- a/workshop-images/base-environment/opt/gateway/src/types/index.d.ts +++ b/workshop-images/base-environment/opt/gateway/src/types/index.d.ts @@ -11,4 +11,4 @@ declare module 'express-session' { started: string page_hits: number } -} \ No newline at end of file +} diff --git a/workshop-images/base-environment/opt/helper/package-lock.json b/workshop-images/base-environment/opt/helper/package-lock.json index 2398de4c..e190a7c3 100644 --- a/workshop-images/base-environment/opt/helper/package-lock.json +++ b/workshop-images/base-environment/opt/helper/package-lock.json @@ -20,12 +20,12 @@ "@types/mocha": "^10.0.0", "@types/node": "^18.11.9", "@types/vscode": "^1.45.0", - "@typescript-eslint/eslint-plugin": "^5.42.1", - "@typescript-eslint/parser": "^5.42.1", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", "@vscode/test-electron": "^2.2.0", "@vscode/vsce": "^2.19.0", "ansi-regex": ">=6.0.1", - "eslint": "^8.27.0", + "eslint": "^8.44.0", "glob": "^8.0.3", "minimist": ">=1.2.7", "mocha": "^10.1.0", @@ -35,6 +35,15 @@ "vscode": "^1.45.0" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -51,23 +60,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", + "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -83,18 +92,18 @@ } }, "node_modules/@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", + "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -222,9 +231,9 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, "node_modules/@types/mime": { @@ -264,9 +273,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "dev": true }, "node_modules/@types/send": { @@ -296,32 +305,34 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", - "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz", + "integrity": "sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/type-utils": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/type-utils": "6.1.0", + "@typescript-eslint/utils": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -330,25 +341,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", - "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.1.0.tgz", + "integrity": "sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -357,16 +369,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", - "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.1.0.tgz", + "integrity": "sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1" + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -374,25 +386,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", - "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.1.0.tgz", + "integrity": "sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/utils": "6.1.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -401,12 +413,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", - "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.1.0.tgz", + "integrity": "sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -414,21 +426,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", - "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.1.0.tgz", + "integrity": "sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -441,42 +453,41 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", - "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.1.0.tgz", + "integrity": "sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/typescript-estree": "6.1.0", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", - "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz", + "integrity": "sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.1", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.1.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -577,9 +588,9 @@ } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1369,16 +1380,16 @@ } }, "node_modules/eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", + "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint/eslintrc": "^2.1.0", + "@eslint/js": "8.44.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", @@ -1388,8 +1399,8 @@ "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.6.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1397,20 +1408,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" @@ -1425,23 +1435,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1558,14 +1555,14 @@ } }, "node_modules/espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1616,15 +1613,6 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1713,9 +1701,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -2032,10 +2020,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "node_modules/has": { @@ -2346,16 +2334,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2970,17 +2948,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -3487,9 +3465,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3831,25 +3809,16 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "node_modules/ts-api-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", + "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, "engines": { - "node": ">= 6" + "node": ">=16.13.0" }, "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + "typescript": ">=4.2.0" } }, "node_modules/tunnel": { @@ -4006,15 +3975,6 @@ "node": ">= 8" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -4197,6 +4157,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -4207,20 +4173,20 @@ } }, "@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", "dev": true }, "@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", + "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -4230,15 +4196,15 @@ } }, "@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", + "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -4344,9 +4310,9 @@ } }, "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, "@types/mime": { @@ -4386,9 +4352,9 @@ "dev": true }, "@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "dev": true }, "@types/send": { @@ -4418,102 +4384,104 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", - "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz", + "integrity": "sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw==", "dev": true, "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/type-utils": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/type-utils": "6.1.0", + "@typescript-eslint/utils": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/parser": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", - "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.1.0.tgz", + "integrity": "sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", - "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.1.0.tgz", + "integrity": "sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1" + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0" } }, "@typescript-eslint/type-utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", - "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.1.0.tgz", + "integrity": "sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.59.1", - "@typescript-eslint/utils": "5.59.1", + "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/utils": "6.1.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", - "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.1.0.tgz", + "integrity": "sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", - "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.1.0.tgz", + "integrity": "sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/visitor-keys": "5.59.1", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/visitor-keys": "6.1.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/utils": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", - "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.1.0.tgz", + "integrity": "sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==", "dev": true, "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.1", - "@typescript-eslint/types": "5.59.1", - "@typescript-eslint/typescript-estree": "5.59.1", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.1.0", + "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/typescript-estree": "6.1.0", + "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "5.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", - "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz", + "integrity": "sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.1", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.1.0", + "eslint-visitor-keys": "^3.4.1" } }, "@vscode/test-electron": { @@ -4589,9 +4557,9 @@ } }, "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "acorn-jsx": { @@ -5170,16 +5138,16 @@ "dev": true }, "eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", + "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint/eslintrc": "^2.1.0", + "@eslint/js": "8.44.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", @@ -5189,8 +5157,8 @@ "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.6.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -5198,20 +5166,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" @@ -5290,31 +5257,21 @@ } } }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, "eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", "dev": true }, "espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", "dev": true, "requires": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" } }, "esquery": { @@ -5351,12 +5308,6 @@ } } }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5435,9 +5386,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -5690,10 +5641,10 @@ "slash": "^3.0.0" } }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "has": { @@ -5911,12 +5862,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -6391,17 +6336,17 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "p-limit": { @@ -6762,9 +6707,9 @@ "dev": true }, "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -7028,20 +6973,12 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "ts-api-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", + "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", "dev": true, - "requires": { - "tslib": "^1.8.1" - } + "requires": {} }, "tunnel": { "version": "0.0.6", @@ -7157,12 +7094,6 @@ "isexe": "^2.0.0" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", diff --git a/workshop-images/base-environment/opt/helper/package.json b/workshop-images/base-environment/opt/helper/package.json index 9d44f735..d0a30bec 100644 --- a/workshop-images/base-environment/opt/helper/package.json +++ b/workshop-images/base-environment/opt/helper/package.json @@ -39,9 +39,9 @@ "@types/mocha": "^10.0.0", "@types/node": "^18.11.9", "@types/vscode": "^1.45.0", - "@typescript-eslint/eslint-plugin": "^5.42.1", - "@typescript-eslint/parser": "^5.42.1", - "eslint": "^8.27.0", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "eslint": "^8.44.0", "glob": "^8.0.3", "mocha": "^10.1.0", "typescript": "^4.8.4", diff --git a/workshop-images/base-environment/opt/renderer/src/backend/modules/config.ts b/workshop-images/base-environment/opt/renderer/src/backend/modules/config.ts index d58a16b9..27a88fbb 100644 --- a/workshop-images/base-environment/opt/renderer/src/backend/modules/config.ts +++ b/workshop-images/base-environment/opt/renderer/src/backend/modules/config.ts @@ -42,13 +42,19 @@ export let config = { // Training portal, workshop and session configuration. platform_arch: process.env.PLATFORM_ARCH || "", - image_repository: process.env.IMAGE_REPOSITORY || "registry.default.svc.cluster.local:5001", + image_repository: process.env.IMAGE_REPOSITORY || "registry.default.svc.cluster.local", + oci_image_cache: process.env.OCI_IMAGE_CACHE || "workshop-images", assets_repository: process.env.ASSETS_REPOSITORY || "workshop-assets", workshop_name: process.env.WORKSHOP_NAME || "workshop", + environment_name: process.env.ENVIRONMENT_NAME || "workshop", + session_name: process.env.SESSION_NAME || "workshop", session_id: process.env.SESSION_ID || "workshop", + session_url: process.env.SESSION_URL || "http://workshop-127-0-0-1.nip.io", session_namespace: process.env.SESSION_NAMESPACE || "workshop", workshop_namespace: process.env.WORKSHOP_NAMESPACE || "workshop", training_portal: process.env.TRAINING_PORTAL || "workshop", + session_hostname: process.env.SESSION_HOSTNAME || "workshop-127-0-0-1.nip.io", + cluster_domain: process.env.CLUSTER_DOMAIN || "cluster.local", ingress_domain: process.env.INGRESS_DOMAIN || "127-0-0-1.nip.io", ingress_protocol: process.env.INGRESS_PROTOCOL || "http", ingress_port_suffix: process.env.INGRESS_PORT_SUFFIX || "", @@ -58,6 +64,7 @@ export let config = { policy_engine: process.env.POLICY_ENGINE || "none", policy_name: process.env.POLICY_NAME || "restricted", services_password: process.env.SERVICES_PASSWORD || "", + config_password: process.env.CONFIG_PASSWORD || "", // Google and Clarity analytics tracking ID. @@ -70,8 +77,7 @@ export let config = { // should be give by "title". Any document title in an AsciiDoc page will // be ignored. If no title is given it will be generated from name of // file. Label on the button to go to next page can be overridden by - // "exit_sign". For the final page, can define "exit_link", if need to - // send users off site, otherwise should never be defined. + // "exit_sign". modules: [ /* @@ -111,12 +117,18 @@ export let config = { config.variables.push({ name: "platform_arch", content: config.platform_arch }) config.variables.push({ name: "image_repository", content: config.image_repository }) +config.variables.push({ name: "oci_image_cache", content: config.oci_image_cache }) config.variables.push({ name: "assets_repository", content: config.assets_repository }) config.variables.push({ name: "workshop_name", content: config.workshop_name }) +config.variables.push({ name: "environment_name", content: config.environment_name }) +config.variables.push({ name: "session_name", content: config.session_name }) config.variables.push({ name: "session_id", content: config.session_id }) +config.variables.push({ name: "session_url", content: config.session_url }) config.variables.push({ name: "session_namespace", content: config.session_namespace }) config.variables.push({ name: "workshop_namespace", content: config.workshop_namespace }) config.variables.push({ name: "training_portal", content: config.training_portal }) +config.variables.push({ name: "session_hostname", content: config.session_hostname }) +config.variables.push({ name: "cluster_domain", content: config.cluster_domain }) config.variables.push({ name: "ingress_domain", content: config.ingress_domain }) config.variables.push({ name: "ingress_protocol", content: config.ingress_protocol }) config.variables.push({ name: "ingress_port_suffix", content: config.ingress_port_suffix }) @@ -126,6 +138,7 @@ config.variables.push({ name: "storage_class", content: config.storage_class }) config.variables.push({ name: "policy_engine", content: config.policy_engine }) config.variables.push({ name: "policy_name", content: config.policy_name }) config.variables.push({ name: "services_password", content: config.services_password }) +config.variables.push({ name: "config_password", content: config.config_password }) if (fs.existsSync("/var/run/secrets/kubernetes.io/serviceaccount/token")) { let data = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token") diff --git a/workshop-images/base-environment/opt/renderer/src/backend/modules/content.ts b/workshop-images/base-environment/opt/renderer/src/backend/modules/content.ts index c79249b0..8281327c 100644 --- a/workshop-images/base-environment/opt/renderer/src/backend/modules/content.ts +++ b/workshop-images/base-environment/opt/renderer/src/backend/modules/content.ts @@ -64,6 +64,7 @@ export function modules() { if (fs.existsSync(file)) { page.file = file page.format = "markdown" + page.fences = "hljs" } } @@ -73,6 +74,7 @@ export function modules() { if (fs.existsSync(file)) { page.file = file page.format = "asciidoc" + page.fences = "" } } diff --git a/workshop-images/base-environment/opt/renderer/src/backend/modules/routes.ts b/workshop-images/base-environment/opt/renderer/src/backend/modules/routes.ts index 958c0b0a..8ab9c018 100644 --- a/workshop-images/base-environment/opt/renderer/src/backend/modules/routes.ts +++ b/workshop-images/base-environment/opt/renderer/src/backend/modules/routes.ts @@ -28,7 +28,7 @@ router.get("/workshop/content/", (req, res) => { if (modules.length == 0) return res.send("No workshop content available.") - res.redirect(path.join(req.originalUrl, modules[0].path)) + res.redirect(path.join("/workshop/content", modules[0].path)) }) // If request matches a static file, serve up the contents immediately. We diff --git a/workshop-images/base-environment/opt/renderer/src/backend/views/content-page.pug b/workshop-images/base-environment/opt/renderer/src/backend/views/content-page.pug index 612b74ad..82dfba88 100644 --- a/workshop-images/base-environment/opt/renderer/src/backend/views/content-page.pug +++ b/workshop-images/base-environment/opt/renderer/src/backend/views/content-page.pug @@ -1,6 +1,8 @@ doctype html html head + meta(name="generator" content=`Educates (${module.format})`) + if module.format == "asciidoc" link(rel="stylesheet", href="/workshop/static/asciidoctor/css/asciidoctor.css") @@ -38,6 +40,7 @@ html data-current-page=module.path, data-next-page=module.next_page, data-page-format=module.format, + data-code-fences=module.fences, data-page-step=module.step, data-pages-total=modules.length) diff --git a/workshop-images/base-environment/opt/renderer/src/backend/views/main-content.pug b/workshop-images/base-environment/opt/renderer/src/backend/views/main-content.pug index bff3679b..6e74ca54 100644 --- a/workshop-images/base-environment/opt/renderer/src/backend/views/main-content.pug +++ b/workshop-images/base-environment/opt/renderer/src/backend/views/main-content.pug @@ -9,5 +9,11 @@ - var exit_sign = module.exit_sign ? module.exit_sign : (module.next_page ? "Continue" : "Restart") - .page-meta.clearfix - button.btn.btn-lg.btn-primary.float-right#next-page(type="button" data-next-page=module.next_page data-exit-link=module.exit_link data-restart-url=config.restart_url aria-label=exit_sign) #{exit_sign} + if module.next_page + .page-meta.clearfix + form(action=`/workshop/content/${module.next_page}`) + button.btn.btn-lg.btn-primary.float-right#next-page(type="submit" aria-label=exit_sign) #{exit_sign} + else if config.restart_url + .page-meta.clearfix + form + button.btn.btn-lg.btn-primary.float-right#next-page(type="submit" aria-label=exit_sign onclick="educates.finished_workshop()") #{exit_sign} diff --git a/workshop-images/base-environment/opt/renderer/src/backend/views/page-header.pug b/workshop-images/base-environment/opt/renderer/src/backend/views/page-header.pug index 7dacc09e..22f87fcf 100644 --- a/workshop-images/base-environment/opt/renderer/src/backend/views/page-header.pug +++ b/workshop-images/base-environment/opt/renderer/src/backend/views/page-header.pug @@ -2,13 +2,27 @@ .row.row-no-gutters .col-sm-12 .btn-group.btn-group-sm(role="group") - button.btn.btn-transparent(type="button" data-goto-page="/" aria-label="Home") - span.fas.fa-home.fa-inverse(aria-hidden="true") + form(action="/workshop/content/") + button.btn.btn-transparent#header-goto-home(type="submit" aria-label="Home") + span.fas.fa-home.fa-inverse(aria-hidden="true") .btn-toolbar.float-right(role="toolbar") .btn-group.btn-group-sm(role="group") - button.btn.btn-transparent#header-prev-page(type="button" data-goto-page=module.prev_page disabled="" aria-label="Prev") - span.fas.fa-arrow-left.fa-inverse(aria-hidden="true") - button.btn.btn-transparent#header-goto-toc(type="button" aria-label="TOC" data-toggle="modal" data-target="#table-of-contents") - span.fas.fa-list.fa-inverse(aria-hidden="true") - button.btn.btn-transparent#header-next-page(type="button" data-goto-page=module.next_page disabled="" aria-label="Next") - span.fas.fa-arrow-right.fa-inverse(aria-hidden="true") + if module.prev_page + form(action=`/workshop/content/${module.prev_page}`) + button.btn.btn-transparent#header-prev-page(type="submit" aria-label="Prev") + span.fas.fa-arrow-left.fa-inverse(aria-hidden="true") + else + form + button.btn.btn-transparent#header-prev-page(type="submit" disabled="" aria-label="Prev") + span.fas.fa-arrow-left.fa-inverse(aria-hidden="true") + form + button.btn.btn-transparent#header-goto-toc(type="button" aria-label="TOC" data-toggle="modal" data-target="#table-of-contents") + span.fas.fa-list.fa-inverse(aria-hidden="true") + if module.next_page + form(action=`/workshop/content/${module.next_page}`) + button.btn.btn-transparent#header-next-page(type="submit" aria-label="Next") + span.fas.fa-arrow-right.fa-inverse(aria-hidden="true") + else + form + button.btn.btn-transparent#header-next-page(type="submit" disabled="" aria-label="Next") + span.fas.fa-arrow-right.fa-inverse(aria-hidden="true") diff --git a/workshop-images/base-environment/opt/renderer/src/frontend/scripts/educates.ts b/workshop-images/base-environment/opt/renderer/src/frontend/scripts/educates.ts index c5a9349c..eada628d 100644 --- a/workshop-images/base-environment/opt/renderer/src/frontend/scripts/educates.ts +++ b/workshop-images/base-environment/opt/renderer/src/frontend/scripts/educates.ts @@ -6,7 +6,7 @@ import * as amplitude from '@amplitude/analytics-browser' // Hack to get jsonform working. -declare var window : any +declare var window: any window.$ = window.jQuery = $ import "underscore" @@ -145,12 +145,13 @@ interface Dashboard { session_owner(): string expose_terminal(name: string): boolean expose_dashboard(name: string): boolean - create_dashboard(name: string, url: string): boolean + create_dashboard(name: string, url: string, focus: boolean): boolean delete_dashboard(name: string): boolean - reload_dashboard(name: string, url?: string): boolean + reload_dashboard(name: string, url: string, focus: boolean): boolean collapse_workshop(): void reload_workshop(): void finished_workshop(): void + terminate_session(): void preview_image(src: string, title: string): void } @@ -487,13 +488,13 @@ export function expose_dashboard(name: string, done = () => { }, fail = (_) => { done() } -export function create_dashboard(name: string, url: string, done = () => { }, fail = (_) => { }) { +export function create_dashboard(name: string, url: string, focus, done = () => { }, fail = (_) => { }) { let dashboard = parent_dashboard() if (!dashboard) return fail("Dashboard is not available") - if (!dashboard.create_dashboard(name, url)) + if (!dashboard.create_dashboard(name, url, focus)) return fail("Dashboard already exists") done() @@ -511,7 +512,7 @@ export function delete_dashboard(name: string, done = () => { }, fail = (_) => { done() } -export function reload_dashboard(name: string, url: string, done = () => { }, fail = (_) => { }) { +export function reload_dashboard(name: string, url: string, focus: boolean = true, done = () => { }, fail = (_) => { }) { let dashboard = parent_dashboard() if (!dashboard) { @@ -519,7 +520,7 @@ export function reload_dashboard(name: string, url: string, done = () => { }, fa return } - if (!dashboard.reload_dashboard(name, url)) + if (!dashboard.reload_dashboard(name, url, focus)) return fail("Dashboard does not exist") done() @@ -554,7 +555,14 @@ export function finished_workshop() { dashboard.finished_workshop() } -function preview_image(src: string, title: string) { +export function terminate_session() { + let dashboard = parent_dashboard() + + if (dashboard) + dashboard.terminate_session() +} + +export function preview_image(src: string, title: string) { let dashboard = parent_dashboard() if (!dashboard) { @@ -618,6 +626,7 @@ export function register_action(options: any) { setup: (args, element) => { }, trigger: (args, element) => { }, finish: (args, element, error) => { }, + pause: 750, cooldown: 1, } @@ -636,6 +645,7 @@ export function register_action(options: any) { let setup: any = options["setup"] let trigger: any = options["trigger"] let finish: any = options["finish"] + let pause: any = options["pause"] let cooldown: number = options["cooldown"] if (name === undefined) @@ -647,12 +657,14 @@ export function register_action(options: any) { let $body = $("body") - let page_format = $body.data("page-format") + let generator = $('meta[name=generator]').attr('content') - if (page_format == "asciidoc") + if (generator.startsWith("Educates (asciidoc)")) selectors = [`.${classname} .content code`] - else + else if (generator.startsWith("Educates (markdown)") || generator.startsWith("Educates (hugo)")) selectors = [`code.language-${classname}`] + else if (generator.startsWith("Docutils ")) + selectors = [`div.highlight-${classname}>div.highlight`] let index = 1 @@ -664,7 +676,7 @@ export function register_action(options: any) { code_element.addClass("magic-code-block") parent_element.addClass("magic-code-block-parent") - if (page_format == "asciidoc") { + if (generator.startsWith("Educates (asciidoc)")) { let root_element = parent_element.parent().parent() root_element.addClass("magic-code-block-root") @@ -802,6 +814,18 @@ export function register_action(options: any) { glyph_element.addClass("fa-check-circle") finish(action_args, parent_element) + + if (action_args.cascade) { + setTimeout(() => { + if (generator.startsWith("Educates (asciidoc)")) { + let root_element = parent_element.parent().parent() + root_element.next(`*[data-action-name]`).children("div.content").children("div.magic-code-block-title").trigger("click") + } + else { + parent_element.next(`*[data-action-name]`).trigger("click") + } + }, action_args.pause || pause) + } }, (error) => { console.log(`[${title_string}] Failure: ${error}`) @@ -847,6 +871,9 @@ export function register_action(options: any) { }) setup(action_args, parent_element) + + if (action_args.autostart) + parent_element.attr("data-action-autostart", "true") }) } } @@ -856,38 +883,7 @@ $(document).ready(async () => { let $body = $("body") - let page_format = $body.data("page-format") - - // Set up page navigation buttons in header and at bottom of pages. - - $("button[data-goto-page]").each((_, element) => { - if ($(element).data("goto-page")) { - $(element).removeAttr("disabled") - $(element).on("click", () => { - location.href = path.join("/workshop/content", $(element).data("goto-page")) - }) - } - else { - $(element).removeClass("fa-inverse") - } - }) - - $("#next-page").on("click", (event) => { - let next_page = $(event.target).data("next-page") - let exit_link = $(event.target).data("exit-link") - let restart_url = $(event.target).data("restart-url") - - let dashboard = parent_dashboard() - - if (next_page) - location.href = path.join("/workshop/content", next_page) - else if (exit_link) - location.href = exit_link - else if (!restart_url || !dashboard) - location.href = "/workshop/content/" - else - finished_workshop() - }) + let generator = $('meta[name=generator]').attr('content') // Ensure clicking on links in content always opens them in a new page // if they are for an external site. @@ -1296,7 +1292,10 @@ $(document).ready(async () => { return args.url }, handler: (args, element, done, fail) => { - create_dashboard(args.name, args.url, done, fail) + let focus = true + if (args.focus !== undefined) + focus = args.focus + create_dashboard(args.name, args.url, args.focus, done, fail) } }) @@ -1334,7 +1333,10 @@ $(document).ready(async () => { return args.url }, handler: (args, element, done, fail) => { - reload_dashboard(args.name, args.url, done, fail) + let focus = true + if (args.focus !== undefined) + focus = args.focus + reload_dashboard(args.name, args.url, focus, done, fail) } }) @@ -1635,17 +1637,9 @@ $(document).ready(async () => { element.hide() title_element.css("pointer-events", "none") } - if (args.autostart) - element.attr("data-examiner-autostart", "true") + // if (args.autostart) + // element.attr("data-examiner-autostart", "true") }, - finish: (args, element, error) => { - if (!args.cascade || error) - return - let name = element.data("action-name") - let title = element.prev() - let index = parseInt(title.data("action-index")) + 1 - $(`[data-action-name='${name}'][data-action-index='${index}']`).trigger("click") - } }) // Register handler for file download and upload actions. @@ -1959,7 +1953,7 @@ $(document).ready(async () => { trigger: (args, element) => { let parent_element = element let name = args.name || "*" - if (page_format == "asciidoc") { + if (generator.startsWith("Educates (asciidoc)")) { let root_element = parent_element.parent().parent() let state_element = root_element if (state_element.attr("data-section-state") == "visible") { @@ -1979,7 +1973,7 @@ $(document).ready(async () => { let element_range = root_element.nextUntil(`.magic-code-block-root[data-action-name='section:end'][data-section-name='${name}']`).filter(`[data-content-name='${name}']`) element_range.show() state_element.attr("data-section-state", "visible") - element_range.filter("[data-action-name='examiner:execute-test'][data-examiner-autostart]").trigger("click") + element_range.filter("[data-action-name][data-action-autostart]").trigger("click") } } else { @@ -2002,14 +1996,14 @@ $(document).ready(async () => { let element_range = parent_element.nextUntil(`.magic-code-block-parent[data-action-name='section:end'][data-section-name='${name}']`).not(":last").filter(`[data-content-name='${name}']`) element_range.show() state_element.attr("data-section-state", "visible") - element_range.filter("[data-action-name='examiner:execute-test'][data-examiner-autostart]").trigger("click") + element_range.filter("[data-action-name][data-action-autostart]").trigger("click") } } }, setup: (args, element) => { let parent_element = element let name = args.name || "*" - if (page_format == "asciidoc") { + if (generator.startsWith("Educates (asciidoc)")) { let root_element = parent_element.parent().parent() root_element.attr("data-section-name", name) } @@ -2020,7 +2014,7 @@ $(document).ready(async () => { let parent_element = element let title_element = parent_element.prev() let state_element - if (page_format == "asciidoc") + if (generator.startsWith("Educates (asciidoc)")) state_element = parent_element.parent().parent() else state_element = title_element @@ -2055,13 +2049,63 @@ $(document).ready(async () => { body: (args) => { return "" }, + trigger: (args, element) => { + let name = args.name || "*" + if (generator.startsWith("Educates (asciidoc)")) { + let root_element = element.parent().parent().prevAll(`.magic-code-block-root[data-action-name='section:begin'][data-section-name='${name}']`).first() + let state_element = root_element + if (state_element.attr("data-section-state") == "visible") { + let element_range = root_element.nextUntil(`.magic-code-block-root[data-action-name='section:end'][data-section-name='${name}']`) + element_range.hide() + $.each(element_range.filter("[data-section-state='visible']"), (_, target) => { + let section = $(target) + let glyph = section.find(".magic-code-block-glyph") + section.attr("data-section-state", "hidden") + glyph.addClass("fa-chevron-down") + glyph.removeClass("fa-chevron-up") + glyph.removeClass("fa-check-circle") + }) + state_element.attr("data-section-state", "hidden") + } + else { + let element_range = root_element.nextUntil(`.magic-code-block-root[data-action-name='section:end'][data-section-name='${name}']`).filter(`[data-content-name='${name}']`) + element_range.show() + state_element.attr("data-section-state", "visible") + } + } + else { + // let root_element = element.parent().parent() + let root_element = element + let start_element = root_element.prevAll(`.magic-code-block-parent[data-action-name='section:begin'][data-section-name='${name}']`).first() + let title_element = start_element.prev() + let state_element = title_element + if (state_element.attr("data-section-state") == "visible") { + let element_range = start_element.nextUntil(`.magic-code-block-parent[data-action-name='section:end'][data-section-name='${name}']`) + element_range.hide() + $.each(element_range.filter("[data-section-state='visible']"), (_, target) => { + let section_element = $(target) + let glyph_element = section_element.children(".magic-code-block-glyph") + section_element.attr("data-section-state", "hidden") + glyph_element.addClass("fa-chevron-down") + glyph_element.removeClass("fa-chevron-up") + glyph_element.removeClass("fa-check-circle") + }) + state_element.attr("data-section-state", "hidden") + } + else { + let element_range = start_element.nextUntil(`.magic-code-block-parent[data-action-name='section:end'][data-section-name='${name}']`).not(":last").filter(`[data-content-name='${name}']`) + element_range.show() + state_element.attr("data-section-state", "visible") + } + } + }, handler: (args, element, done, fail) => { - fail() + done() }, setup: (args, element) => { let parent_element = element let name = args.name || "*" - if (page_format == "asciidoc") { + if (generator.startsWith("Educates (asciidoc)")) { let root_element = parent_element.parent().parent() root_element.attr("data-section-name", name) root_element.attr("data-content-name", name) @@ -2084,12 +2128,13 @@ $(document).ready(async () => { parent_element.hide() } } - } + }, + pause: 0, }) // Trigger autostart examiner actions at top level. - $("[data-examiner-autostart='true']").not("[data-content-name]").trigger("click") + $("[data-action-autostart='true']").not("[data-content-name]").trigger("click") // Generate analytics event if a tracking ID is provided. diff --git a/workshop-images/base-environment/opt/renderer/src/frontend/styles/educates.css b/workshop-images/base-environment/opt/renderer/src/frontend/styles/educates.css index e9fbefe0..9361a6a8 100644 --- a/workshop-images/base-environment/opt/renderer/src/frontend/styles/educates.css +++ b/workshop-images/base-environment/opt/renderer/src/frontend/styles/educates.css @@ -40,6 +40,42 @@ body { line-height: 30px; } +button#header-goto-home { + border: 0px; + padding-left: 8px; + padding-right: 8px; + padding-top: 4px; + padding-bottom: 4px; + font-size: 0.95em; +} + +button#header-prev-page { + border: 0px; + padding-left: 8px; + padding-right: 8px; + padding-top: 4px; + padding-bottom: 4px; + font-size: 0.95em; +} + +button#header-goto-toc { + border: 0px; + padding-left: 8px; + padding-right: 8px; + padding-top: 4px; + padding-bottom: 4px; + font-size: 0.95em; +} + +button#header-next-page { + border: 0px; + padding-left: 8px; + padding-right: 8px; + padding-top: 4px; + padding-bottom: 4px; + font-size: 0.95em; +} + .menu { padding: 0; margin: 0px 5px; @@ -253,3 +289,38 @@ span.magic-code-block-glyph { word-wrap: break-word; display: inline; } + +.admonition { + padding: 10px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 0px; + text-align: left; +} + +.admonition.note { + color: #2e6b89; + background-color: #e2f0f7; + border-color: #bce8f1; +} + +.admonition.warning { + color: #7a6032; + background-color: #fffae5; + border-color: #fbeed5; +} + +.admonition.danger { + color: #7f3130; + background-color: #fde3e3; + border-color: #eed3d7; +} + +.admonition-title { + font-weight: bold; + text-align: left; +} + +.admonition>p:last-child { + margin-bottom: 0; +} diff --git a/workshop-images/conda-environment/README.md b/workshop-images/conda-environment/README.md index c22390a7..bf51b727 100644 --- a/workshop-images/conda-environment/README.md +++ b/workshop-images/conda-environment/README.md @@ -20,7 +20,7 @@ spec: title: Jupyter Workshop description: Workshop on using Jupyter notebooks. content: - image: registry.default.svc.cluster.local:5001/conda-environment:master + image: registry.default.svc.cluster.local/conda-environment:master files: github.com/eduk8s-tests/lab-jupyter-workshop session: budget: medium