-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Martin Goldman
committed
Aug 20, 2024
1 parent
30867c0
commit 549bb73
Showing
11 changed files
with
590 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# Copyright 2022 Skytap Inc. | ||
|
||
# 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. | ||
|
||
name: Package all CS templates | ||
|
||
on: | ||
workflow_dispatch: | ||
|
||
push: | ||
branches-ignore: | ||
- master | ||
paths: | ||
- "cs-templates/**" | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout code | ||
uses: actions/checkout@v3 | ||
- name: Create zip files | ||
working-directory: ./cs-templates | ||
run: | | ||
rm -f *.tar.gz | ||
for item in *; do | ||
if [ -d "$item" ]; then | ||
pushd $item && tar cvzf ../$item.tar.gz * && popd | ||
fi | ||
done | ||
- name: Push new packages to repo | ||
uses: EndBug/add-and-commit@d4d066316a2a85974a05efb42be78f897793c6d9 | ||
with: | ||
fetch: true | ||
default_author: github_actions | ||
add: cs-templates/*.tar.gz | ||
message: Updating script template packages | ||
push: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# frozen_string_literal: true | ||
|
||
source "https://rubygems.org" | ||
|
||
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } | ||
|
||
gem "faker" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
GEM | ||
remote: https://rubygems.org/ | ||
specs: | ||
concurrent-ruby (1.3.4) | ||
faker (3.4.2) | ||
i18n (>= 1.8.11, < 2) | ||
i18n (1.14.5) | ||
concurrent-ruby (~> 1.0) | ||
|
||
PLATFORMS | ||
x86_64-linux | ||
|
||
DEPENDENCIES | ||
faker | ||
|
||
BUNDLED WITH | ||
2.4.6 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
# Ruby Script Template | ||
|
||
This is a code template that can be used as a starting point for developing a Course Manager Script in Ruby. | ||
|
||
## Requirements | ||
|
||
This template and the supporting scripts should work on Linux and macOS. | ||
|
||
In addition, building a script from this template requires Docker to run containers. The `docker` utility should be on your system path. | ||
|
||
Access to the Course Manager API is required to build and publish packages to Course Manager. Before getting started, login as an administrator to Course Manager, access Admin menu > API keys, and create a new API key/secret pair to use. If you do not see this menu option, please contact Skytap Support to have API access enabled for your account. | ||
|
||
Scripts developed from this template require **Course Manager Script Host v10 or higher** for full compatibility. | ||
|
||
## Using This Template | ||
|
||
* Make a copy of this template to a new directory, ensuring that symlinks are followed (e.g. using `cp -rL`). It may be most convenient to download a fresh copy using a command like: | ||
``` | ||
curl -LO https://github.com/skytap/course-manager-examples/raw/master/script-templates/ruby.zip && unzip -d myscript ruby.zip | ||
``` | ||
* The `script` directory is where your code will go. `script/script.rb` is the entry point -- replace the sample code it contains with your own. You're welcome to add other files and directories under `script/` for use in the script. | ||
* Add any gems required by your script to `script/Gemfile`. Gems will be built in a Linux container to ensure the architecture matches the runtime environment of the Script Host. | ||
* To test running your script, run the `bin/run` command. This will run your code in a Linux container to match the runtime environment of the Script Host. | ||
* To publish your script to Course Manager, run `bin/publish`. This will build the dependencies, create a ZIP package, and push it to your Course Manager course. Upon first run, you will be prompted for the necessary details, which will be saved in a ` .publish.yml` file for subsequent runs. | ||
## Accessing Metadata & Control Endpoint From Your Script | ||
The Skytap Metadata Service provides read-only metadata about the Skytap environment hosting an end user's lab. The Course Manager Control Endpoint provides metadata oriented around the end user lab itself, and it also allows limited modifications of the metadata and state of the lab. This template provides `SkytapMetadata` and `LabControl` classes, which provide lightweight interfaces to these two service endpoints that can be used from your script code. These classes make it easier to consume the Metadata and Control Endpoint services. In addition, they make it easier to develop your scripts locally. | ||
### SkytapMetadata Interface | ||
The `SkytapMetadata` class is required in your `script.rb` by default: | ||
``` | ||
require "skytap_metadata" | ||
``` | ||
`SkytapMetadata` is a singleton. To use it, get a reference to its instance: | ||
``` | ||
metadata = SkytapMetadata.get | ||
``` | ||
Then, you can call methods as follows: | ||
``` | ||
metadata.metadata # => returns Skytap metadata as a hash | ||
metadata.user_data # => parses the Skytap metadata's "user_data" attribute, which is typically JSON for Course Manager-provisioned labs, as a hash and then returns it | ||
metadata.configuration_user_data # => parses the Skytap metadata's "configuration_user_data" attribute, which is typically JSON for Course Manager-provisioned labs, as a hash and returns it | ||
metadata.control_url # => returns the control endpoint URL | ||
``` | ||
### LabControl Interface | ||
The `LabControl` class is required in your `script.rb` by default: | ||
``` | ||
require "lab_control" | ||
``` | ||
`LabControl` is a singleton. To use it, get a reference to its instance: | ||
``` | ||
control = LabControl.get | ||
``` | ||
Then, you can call methods as follows: | ||
``` | ||
control.control_data # => returns control metadata as a hash | ||
control.update_control_data(data) # => updates control data (see below) | ||
control.refresh_content_pane # => requests any open content panes for the lab to refresh | ||
control.refresh_lab # => requests any open learning consoles for the lab to refresh their Skytap environment view | ||
control.find_metadata_attr('myMetadataKey') # => finds and returns a standard or sensitive metadata attribute with the specified name on the lab / event participant, event, course, user, or feature, in that order | ||
control.find_metadata_attr('myMetadataKey', 'metadata') # => same as above but limited to standard metadata | ||
control.find_metadata_attr('myMetadataKey', 'sensitive_metadata') # => same as above but limited to sensitive metadata | ||
|
||
``` | ||
#### Updating Control Data | ||
The `update_control_data` method can be used to achieve the following: | ||
Change runstate: | ||
``` | ||
control.update_control_data(runstate: "running") # or "suspended", "halted", "stopped" | ||
``` | ||
Update metadata or sensitive metadata: | ||
``` | ||
control.update_control_data({ metadata: { AcmeDataProUsername: "user_assigned_from_script" }, sensitive_metadata: { AcmeDataProPassword: "password_assigned_from_script" } }) | ||
``` | ||
Update metadata or sensitive metadata for the associated `course`, `feature` (Events or Labs), `event` (for event participants only), or `user` (for on-demand labs provisioned via the [Request Portal workflow](https://help.skytap.com/course-manager-use-request-portal.html) only): | ||
``` | ||
control.update_control_data({ course: { metadata: { course_last_provisioned: '07/17/2023 17:48:32'} }, feature: { sensitive_metadata: { password: 'secret'} } }) | ||
``` | ||
### Metadata Stub Service | ||
A challenge in developing scripts that interact with lab metadata is that it is only available from within a Skytap environment. To help with this, the `bin/run` script runs a "metadata stub" service, simulating the behavior of the Metadata Service and Control Endpoint locally and returning stubbed data. If you would like to modify the stubbed data returned when running your script locally, simply modify the files in `lib/script_support/stub_data`. | ||
## License | ||
Copyright 2023 Skytap Inc. | ||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
build: | ||
name: Build | ||
image_name: ruby:3.2-bullseye | ||
command: ["/bin/bash", "-c", "bundle install && bundle config deployment true && bundle package"] | ||
persistent_artifacts: | ||
- Gemfile.lock | ||
run: | ||
name: Run | ||
image_name: ruby:3.2-bullseye | ||
command: ["/bin/bash", "-c", "bundle config deployment true && bundle exec ruby script.rb"] | ||
env: | ||
- "RUBYLIB=./lib" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# Copyright 2023 Skytap Inc. | ||
|
||
# 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. | ||
|
||
require "json" | ||
require "net/http" | ||
|
||
class APIHelper | ||
class OperationFailedError < StandardError; end | ||
|
||
def self.rest_call(url, verb, data=nil) | ||
uri = URI(url) | ||
host, port, path = uri.host, uri.port, uri.path | ||
path = "/" if path == "" | ||
data = data.to_json if data.kind_of?(Hash) | ||
|
||
http = Net::HTTP.new(host, port) | ||
http.use_ssl = true if uri.instance_of?(URI::HTTPS) | ||
|
||
req = Object.const_get("Net::HTTP::#{verb.capitalize}").new(path) | ||
req.body = data | ||
req["Content-Type"] = "application/json" | ||
|
||
result = http.request(req) | ||
raise OperationFailedError, "#{result.code} #{result.message}" unless result.code == "200" | ||
result.body | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
# Copyright 2023 Skytap Inc. | ||
|
||
# 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. | ||
|
||
require "json" | ||
require_relative "skytap_metadata" | ||
require_relative "api_helper" | ||
|
||
class LabControl | ||
def self.get | ||
@lab_control ||= | ||
if control_url = ENV["LAB_CONTROL_PROXY_URL"] | ||
LiveLabControl.new(control_url) | ||
else | ||
StubbedLabControl.new | ||
end | ||
end | ||
|
||
def control_data | ||
@control_data ||= JSON.parse(control_data_json) | ||
end | ||
|
||
def update_control_data(data) = raise NotImplementedError | ||
def refresh_content_pane = raise NotImplementedError | ||
def refresh_lab = raise NotImplementedError | ||
|
||
def find_metadata_attr(key, within = nil) | ||
collections = within ? [within] : ['metadata', 'sensitive_metadata'] | ||
collections.each do |collection| | ||
[nil, 'event', 'course', 'user', 'feature'].each do |obj| | ||
if this_level_value = control_data.dig(*[obj, collection, key].compact) | ||
return this_level_value | ||
end | ||
end | ||
end | ||
|
||
nil | ||
end | ||
|
||
private | ||
|
||
def control_data_json = raise NotImplementedError | ||
end | ||
|
||
class LiveLabControl < LabControl | ||
def initialize(control_url) | ||
@control_url = control_url | ||
end | ||
|
||
def update_control_data(data) | ||
result_body = APIHelper.rest_call(@control_url, "put", data) | ||
@control_data_json = result_body | ||
end | ||
|
||
def refresh_content_pane = lab_broadcast(:refresh_content_pane) | ||
|
||
def refresh_lab = lab_broadcast(:refresh_lab) | ||
|
||
private | ||
|
||
def lab_broadcast(type) | ||
broadcast_url = "#{control_data['user_access_url']}/learning_console/broadcast" | ||
APIHelper.rest_call(broadcast_url, "post", {type: type}) | ||
end | ||
|
||
def control_data_json | ||
@control_data_json ||= APIHelper.rest_call(@control_url, "get") | ||
end | ||
end | ||
|
||
class StubbedLabControl < LabControl | ||
def refresh_content_pane = nil | ||
def refresh_lab = nil | ||
|
||
def control_data_json | ||
@control_data_json ||= | ||
File.read( | ||
File.join(File.dirname(__FILE__), "stub_data/control_data.json") | ||
) | ||
end | ||
|
||
def update_control_data(data) | ||
METADATA_FIELDS.each do |keys| | ||
incoming_metadata = data.dig(*keys) | ||
update_metadata(keys, incoming_metadata) if incoming_metadata.is_a? Hash | ||
end | ||
control_data | ||
end | ||
|
||
METADATA_FIELDS = [ | ||
['metadata'], ['sensitive_metadata'], | ||
['feature', 'metadata'], ['feature', 'sensitive_metadata'], | ||
['course', 'metadata'], ['course', 'sensitive_metadata'], | ||
['user', 'metadata'], ['user', 'sensitive_metadata'], | ||
['event', 'metadata'], ['event', 'sensitive_metadata'], | ||
].freeze | ||
private_constant :METADATA_FIELDS | ||
|
||
private | ||
|
||
def update_metadata(keys, incoming_metadata) | ||
control_data[keys.first] ||= {} | ||
if keys.length > 1 | ||
control_data[keys.first][keys[1]] ||= {} if keys[1] | ||
control_data[keys.first][keys[1]].merge!(incoming_metadata).compact! | ||
else | ||
control_data[keys.first].merge!(incoming_metadata).compact! | ||
end | ||
end | ||
end |
Oops, something went wrong.