Skip to content

Commit

Permalink
ruby cs template - first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Martin Goldman committed Aug 20, 2024
1 parent 30867c0 commit 549bb73
Show file tree
Hide file tree
Showing 11 changed files with 590 additions and 0 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/package_cs_templates.yml
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
7 changes: 7 additions & 0 deletions cs-templates/ruby/Gemfile
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"
17 changes: 17 additions & 0 deletions cs-templates/ruby/Gemfile.lock
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
117 changes: 117 additions & 0 deletions cs-templates/ruby/README.md
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.
12 changes: 12 additions & 0 deletions cs-templates/ruby/config.yml
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"
38 changes: 38 additions & 0 deletions cs-templates/ruby/lib/api_helper.rb
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
120 changes: 120 additions & 0 deletions cs-templates/ruby/lib/lab_control.rb
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
Loading

0 comments on commit 549bb73

Please sign in to comment.