From f42759a4e66fab179acfc411a082247b02dd140b Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Fri, 9 Aug 2024 08:09:51 -0600 Subject: [PATCH] Initial commit --- .github/workflows/buildService.yml | 37 ++++++ .github/workflows/releaseService.yml | 72 ++++++++++++ .gitignore | 5 + .gitmodules | 3 + Dockerfile | 11 ++ LICENSE | 21 ++++ Makefile | 81 +++++++++++++ README.md | 121 +++++++++++++++++++ docker_entrypoint.sh | 4 + hello-world | 1 + icon.png | Bin 0 -> 33374 bytes instructions.md | 5 + manifest.yaml | 167 +++++++++++++++++++++++++++ scripts/deps.ts | 1 + scripts/embassy.ts | 5 + scripts/procedures/getConfig.ts | 5 + scripts/procedures/healthChecks.ts | 7 ++ scripts/procedures/migrations.ts | 4 + scripts/procedures/properties.ts | 3 + scripts/procedures/setConfig.ts | 5 + 20 files changed, 558 insertions(+) create mode 100644 .github/workflows/buildService.yml create mode 100644 .github/workflows/releaseService.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100755 docker_entrypoint.sh create mode 160000 hello-world create mode 100644 icon.png create mode 100644 instructions.md create mode 100644 manifest.yaml create mode 100644 scripts/deps.ts create mode 100644 scripts/embassy.ts create mode 100644 scripts/procedures/getConfig.ts create mode 100644 scripts/procedures/healthChecks.ts create mode 100644 scripts/procedures/migrations.ts create mode 100644 scripts/procedures/properties.ts create mode 100644 scripts/procedures/setConfig.ts diff --git a/.github/workflows/buildService.yml b/.github/workflows/buildService.yml new file mode 100644 index 0000000..ab6e729 --- /dev/null +++ b/.github/workflows/buildService.yml @@ -0,0 +1,37 @@ +name: Build Service + +on: + workflow_dispatch: + pull_request: + paths-ignore: ['*.md'] + branches: ['main', 'master'] + push: + paths-ignore: ['*.md'] + branches: ['main', 'master'] + +jobs: + BuildPackage: + runs-on: ubuntu-latest + steps: + - name: Prepare StartOS SDK + uses: Start9Labs/sdk@v1 + + - name: Checkout services repository + uses: actions/checkout@v4 + + - name: Build the service package + id: build + run: | + git submodule update --init --recursive + start-sdk init + make + PACKAGE_ID=$(yq -oy ".id" manifest.*) + echo "package_id=$PACKAGE_ID" >> $GITHUB_ENV + printf "\n SHA256SUM: $(sha256sum ${PACKAGE_ID}.s9pk) \n" + shell: bash + + - name: Upload .s9pk + uses: actions/upload-artifact@v4 + with: + name: ${{ env.package_id }}.s9pk + path: ./${{ env.package_id }}.s9pk diff --git a/.github/workflows/releaseService.yml b/.github/workflows/releaseService.yml new file mode 100644 index 0000000..6cf91f2 --- /dev/null +++ b/.github/workflows/releaseService.yml @@ -0,0 +1,72 @@ +name: Release Service + +on: + push: + tags: + - 'v*.*' + +jobs: + ReleasePackage: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Prepare StartOS SDK + uses: Start9Labs/sdk@v1 + + - name: Checkout services repository + uses: actions/checkout@v4 + + - name: Build the service package + run: | + git submodule update --init --recursive + start-sdk init + make + + - name: Setting package ID and title from the manifest + id: package + run: | + echo "package_id=$(yq -oy ".id" manifest.*)" >> $GITHUB_ENV + echo "package_title=$(yq -oy ".title" manifest.*)" >> $GITHUB_ENV + shell: bash + + - name: Generate sha256 checksum + run: | + PACKAGE_ID=${{ env.package_id }} + printf "\n SHA256SUM: $(sha256sum ${PACKAGE_ID}.s9pk) \n" + sha256sum ${PACKAGE_ID}.s9pk > ${PACKAGE_ID}.s9pk.sha256 + shell: bash + + - name: Generate changelog + run: | + PACKAGE_ID=${{ env.package_id }} + echo "## What's Changed" > change-log.txt + yq -oy '.release-notes' manifest.* >> change-log.txt + echo "## SHA256 Hash" >> change-log.txt + echo '```' >> change-log.txt + sha256sum ${PACKAGE_ID}.s9pk >> change-log.txt + echo '```' >> change-log.txt + shell: bash + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: ${{ env.package_title }} ${{ github.ref_name }} + prerelease: true + body_path: change-log.txt + files: | + ./${{ env.package_id }}.s9pk + ./${{ env.package_id }}.s9pk.sha256 + + - name: Publish to Registry + env: + S9USER: ${{ secrets.S9USER }} + S9PASS: ${{ secrets.S9PASS }} + S9REGISTRY: ${{ secrets.S9REGISTRY }} + run: | + if [[ -z "$S9USER" || -z "$S9PASS" || -z "$S9REGISTRY" ]]; then + echo "Publish skipped: missing registry credentials." + else + start-sdk publish https://$S9USER:$S9PASS@$S9REGISTRY ${{ env.package_id }}.s9pk + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9567687 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.s9pk +scripts/*.js +.DS_Store +.vscode/ +docker-images \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..56fa461 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "hello-world"] + path = hello-world + url = https://github.com/Start9Labs/hello-world diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b37541e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine:3.20 + +RUN apk update +RUN apk add --no-cache tini && \ + rm -f /var/cache/apk/* + +ARG ARCH +ADD ./hello-world/target/${ARCH}-unknown-linux-musl/release/hello-world /usr/local/bin/hello-world +RUN chmod +x /usr/local/bin/hello-world +ADD ./docker_entrypoint.sh /usr/local/bin/docker_entrypoint.sh +RUN chmod a+x /usr/local/bin/docker_entrypoint.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..599ad42 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Start9 Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e69f277 --- /dev/null +++ b/Makefile @@ -0,0 +1,81 @@ +PKG_ID := $(shell yq e ".id" manifest.yaml) +PKG_VERSION := $(shell yq e ".version" manifest.yaml) +TS_FILES := $(shell find ./ -name \*.ts) +HELLO_WORLD_SRC := $(shell find ./hello-world/src) hello-world/Cargo.toml hello-world/Cargo.lock + +# delete the target of a rule if it has changed and its recipe exits with a nonzero exit status +.DELETE_ON_ERROR: + +all: verify + +verify: $(PKG_ID).s9pk + @start-sdk verify s9pk $(PKG_ID).s9pk + @echo " Done!" + @echo " Filesize: $(shell du -h $(PKG_ID).s9pk) is ready" + +install: + @if [ ! -f ~/.embassy/config.yaml ]; then echo "You must define \"host: http://server-name.local\" in ~/.embassy/config.yaml config file first."; exit 1; fi + @echo "\nInstalling to $$(grep -v '^#' ~/.embassy/config.yaml | cut -d'/' -f3) ...\n" + @[ -f $(PKG_ID).s9pk ] || ( $(MAKE) && echo "\nInstalling to $$(grep -v '^#' ~/.embassy/config.yaml | cut -d'/' -f3) ...\n" ) + @start-cli package install $(PKG_ID).s9pk + +clean: + rm -rf docker-images + rm -f $(PKG_ID).s9pk + rm -f scripts/*.js + +clean-manifest: + @sed -i '' '/^[[:blank:]]*#/d' manifest.yaml + @echo; echo "Comments successfully removed from manifest.yaml file."; echo + +# BEGIN REBRANDING +rebranding: + @read -p "Enter new package ID name (must be a single word): " NEW_PKG_ID; \ + read -p "Enter new package title: " NEW_PKG_TITLE; \ + find . \( -name "*.md" -o -name ".gitignore" -o -name "manifest.yaml" -o -name "*Service.yml" \) -type f -not -path "./hello-world/*" -exec sed -i '' -e "s/hello-world/$$NEW_PKG_ID/g; s/Hello World/$$NEW_PKG_TITLE/g" {} +; \ + echo; echo "Rebranding complete."; echo " New package ID name is: $$NEW_PKG_ID"; \ + echo " New package title is: $$NEW_PKG_TITLE"; \ + sed -i '' -e '/^# BEGIN REBRANDING/,/^# END REBRANDING/ s/^#*/#/' Makefile + @echo; echo "Note: Rebranding code has been commented out in Makefile"; echo +# END REBRANDING + +scripts/embassy.js: $(TS_FILES) + deno bundle scripts/embassy.ts scripts/embassy.js + +arm: + @rm -f docker-images/x86_64.tar + ARCH=aarch64 $(MAKE) + +x86: + @rm -f docker-images/aarch64.tar + ARCH=x86_64 $(MAKE) + +docker-images/aarch64.tar: Dockerfile docker_entrypoint.sh hello-world/target/aarch64-unknown-linux-musl/release/hello-world +ifeq ($(ARCH),x86_64) +else + mkdir -p docker-images + docker buildx build --tag start9/$(PKG_ID)/main:$(PKG_VERSION) --build-arg ARCH=aarch64 --platform=linux/arm64 -o type=docker,dest=docker-images/aarch64.tar . +endif + +docker-images/x86_64.tar: Dockerfile docker_entrypoint.sh hello-world/target/x86_64-unknown-linux-musl/release/hello-world +ifeq ($(ARCH),aarch64) +else + mkdir -p docker-images + docker buildx build --tag start9/$(PKG_ID)/main:$(PKG_VERSION) --build-arg ARCH=x86_64 --platform=linux/amd64 -o type=docker,dest=docker-images/x86_64.tar . +endif + +$(PKG_ID).s9pk: manifest.yaml instructions.md icon.png LICENSE scripts/embassy.js docker-images/aarch64.tar docker-images/x86_64.tar +ifeq ($(ARCH),aarch64) + @echo "start-sdk: Preparing aarch64 package ..." +else ifeq ($(ARCH),x86_64) + @echo "start-sdk: Preparing x86_64 package ..." +else + @echo "start-sdk: Preparing Universal Package ..." +endif + @start-sdk pack + +hello-world/target/aarch64-unknown-linux-musl/release/hello-world: $(HELLO_WORLD_SRC) + docker run --rm -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)"/hello-world:/home/rust/src messense/rust-musl-cross:aarch64-musl cargo build --release + +hello-world/target/x86_64-unknown-linux-musl/release/hello-world: $(HELLO_WORLD_SRC) + docker run --rm -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)"/hello-world:/home/rust/src messense/rust-musl-cross:x86_64-musl cargo build --release diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9a0308 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +

+ Project Logo +

+ +# Hello World for StartOS + +Hello World is a simple, minimal project that serves as a template for creating a service that runs on StartOS. This repository creates the `s9pk` package that is installed to run `hello-world` on [StartOS](https://github.com/Start9Labs/start-os/). Learn more about service packaging in the [Developer Docs](https://start9.com/latest/developer-docs/). + +## Dependencies + +Install the system dependencies below to build this project by following the instructions in the provided links. You can find instructions on how to set up the appropriate build environment in the [Developer Docs](https://docs.start9.com/latest/developer-docs/packaging). + +- [docker](https://docs.docker.com/get-docker) +- [docker-buildx](https://docs.docker.com/buildx/working-with-buildx/) +- [yq](https://mikefarah.gitbook.io/yq) +- [deno](https://deno.land/) +- [make](https://www.gnu.org/software/make/) +- [start-sdk](https://github.com/Start9Labs/start-os/tree/sdk/) + +## Build environment +Prepare your StartOS build environment. In this example we are using Ubuntu 20.04. +1. Install docker +``` +curl -fsSL https://get.docker.com | bash +sudo usermod -aG docker "$USER" +exec sudo su -l $USER +``` +2. Set buildx as the default builder +``` +docker buildx install +docker buildx create --use +``` +3. Enable cross-arch emulated builds in docker +``` +docker run --privileged --rm linuxkit/binfmt:v0.8 +``` +4. Install yq +``` +sudo snap install yq +``` +5. Install deno +``` +sudo snap install deno +``` +6. Install essentials build packages +``` +sudo apt-get install -y build-essential openssl libssl-dev libc6-dev clang libclang-dev ca-certificates +``` +7. Install Rust +``` +curl https://sh.rustup.rs -sSf | sh +# Choose nr 1 (default install) +source $HOME/.cargo/env +``` +8. Build and install start-sdk +``` +git clone https://github.com/Start9Labs/start-os.git && \ + cd start-os && git submodule update --init --recursive && \ + make sdk +``` +Initialize sdk & verify install +``` +start-sdk init +start-sdk --version +``` +Now you are ready to build the `hello-world` package! + +## Cloning + +Clone the project locally: + +``` +git clone https://github.com/Start9Labs/hello-world-startos.git +cd hello-world-startos +git submodule update --init --recursive +``` + +## Building + +To build the `hello-world` package for all platforms using start-sdk, run the following command: + +``` +make +``` + +To build the `hello-world` package for a single platform using start-sdk, run: + +``` +# for amd64 +make x86 +``` +or +``` +# for arm64 +make arm +``` + +## Installing (on StartOS) + +Run the following commands to determine successful install: +> :information_source: Change server-name.local to your Start9 server address + +``` +start-cli auth login +# Enter your StartOS password +start-cli --host https://server-name.local package install hello-world.s9pk +``` + +If you already have your `start-cli` config file setup with a default `host`, you can install simply by running: + +``` +make install +``` + +> **Tip:** You can also install the hello-world.s9pk using **Sideload Service** under the **System > Manage** section. + +### Verify Install + +Go to your StartOS Services page, select **Hello World**, configure and start the service. Then, verify its interfaces are accessible. + +**Done!** diff --git a/docker_entrypoint.sh b/docker_entrypoint.sh new file mode 100755 index 0000000..3c486bd --- /dev/null +++ b/docker_entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +printf "\n\n [i] Starting Hello World ...\n\n" +exec tini hello-world diff --git a/hello-world b/hello-world new file mode 160000 index 0000000..48ce8a5 --- /dev/null +++ b/hello-world @@ -0,0 +1 @@ +Subproject commit 48ce8a519b14eb6c32d770af66656e04a7866228 diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8bbdd324d7633d87ebc58a410aac08322462eb GIT binary patch literal 33374 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

a;e`Vb4g&{Da`TloDwi%fgjQU8W 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 bBUAn#2%7&qEQm!@00000NkvXXu0mjf?8@O9 literal 0 HcmV?d00001 diff --git a/instructions.md b/instructions.md new file mode 100644 index 0000000..c636962 --- /dev/null +++ b/instructions.md @@ -0,0 +1,5 @@ +# Instructions for Hello World + +Instructions go here. These appear to the user in the UI on the Service page under **Instructions**. + +You are allowed to include basic [Markdown formatting](https://www.markdownguide.org/basic-syntax). diff --git a/manifest.yaml b/manifest.yaml new file mode 100644 index 0000000..832c773 --- /dev/null +++ b/manifest.yaml @@ -0,0 +1,167 @@ +# Example written in yaml (toml and json are also acceptable) + +# The package identifier used by the OS. This must be unique amongst all other known packages +id: hello-world + # A human readable service title +title: "Hello World" +# Service version - accepts up to four digits, where the last confirms to revisions necessary for StartOS - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of the service. +version: 0.3.5 +# Release notes for the update - can be a string, paragraph or URL +release-notes: | + Revamped for StartOS 0.3.5.x + - Changed version to 0.3.5 + - Updated manifest with correct OS name, GPU acceleration support, and supported architectures. + - Added release and build GitHub workflows. + - Added log message to announce when the service is starting. + - Added new optimized and standardized icon. + - Added simple healthcheck. + - Updated to the latest stable Alpine-based Docker image. + - Updated license file with the current year. + - Code cleanup +# The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package. +license: MIT +# The repository URL for the package. This repo contains the manifest file (this), any scripts necessary for configuration, backups, actions, or health checks (more below). This key must exist. But could be embedded into the source repository. +wrapper-repo: "https://github.com/Start9Labs/hello-world-startos" +# The original project repository URL. There is no upstream repo in this example +upstream-repo: "https://github.com/Start9Labs/hello-world" +# URL to the support site / channel for the project. This key can be omitted if none exists, or it can link to the original project repository issues. +support-site: "https://docs.start9.com/" +# URL to the marketing site for the project. This key can be omitted if none exists, or it can link to the original project repository. +marketing-site: "https://start9.com/" +# The series of commands to build the project into an s9pk for arm64/v8. In this case we are using a Makefile with the simple build command "make". +build: ["make"] +# Human readable descriptors for the service. These are used throughout the StartOS user interface, primarily in the marketplace. +description: + # This is the first description visible to the user in the marketplace. + short: Example service + # This description will display with additional details in the service's individual marketplace page + long: | + Hello World is a bare-bones service that launches a web interface to say "Hello World", and nothing more. +# These assets are static files necessary for packaging the service for Start9 (into an s9pk). Each value is a path to the specified asset. If an asset is missing from this list, or otherwise denoted, it will be defaulted to the values denoted below. +assets: + # Default = LICENSE.md + license: LICENSE + # Default = icon.png + icon: icon.png + # Default = INSTRUCTIONS.md + instructions: instructions.md + # Default = image.tar + # docker-images: image.tar +# ----- This section commented out until we support long-running containers ----- +# The main action for initializing the service. This can be script to utilize the eOS scripting apis, or docker. +# main: +# type: script +# # Defines the containers needed to run the main and mounted volumes +# containers: +# main: +# # Identifier for the main image volume, which will be used when other actions need to mount to this volume. +# image: main +# # Specifies where to mount the data volume(s), if there are any. Mounts for pointer dependency volumes are also denoted here. These are necessary if data needs to be read from / written to these volumes. +# mounts: +# # Specifies where on the service's file system its persistence directory should be mounted prior to service startup +# main: /root +# ----- END commented section ----- +main: + # Docker is currently the only action implementation + type: docker + # Identifier for the main image volume, which will be used when other actions need to mount to this volume. + image: main + # The executable binary for starting the initialization action. For docker actions, this is typically a "docker_entrypoint.sh" file. See the Dockerfile and the docker_entrypoint.sh in this project for additional details. + entrypoint: "docker_entrypoint.sh" + # Any arguments that should be passed into the entrypoint executable + args: [] + # Specifies where to mount the data volume(s), if there are any. Mounts for pointer dependency volumes are also denoted here. These are necessary if data needs to be read from / written to these volumes. + mounts: + # Specifies where on the service's file system its persistence directory should be mounted prior to service startup + main: /root +# Specifies whether GPU acceleration is enabled or not. False by default. + gpu-acceleration: false +# Defines what architectures will be supported by the service. This service supports x86_64 and aarch64 architectures. +hardware-requirements: + arch: + - x86_64 + - aarch64 +# This is where health checks would be defined - see a more advanced example in https://github.com/Start9Labs/start9-pages-startos +health-checks: + web-ui: + name: Web Interface + success-message: The Hello World is accessible + type: script +config: ~ +properties: ~ +# type: script +# This denotes any data, asset, or pointer volumes that should be connected when the "docker run" command is invoked +volumes: + # This is the image where files from the project asset directory will go + main: + type: data +# This specifies how to configure the port mapping for exposing the service over TOR and LAN (if applicable). Many interfaces can be specified depending on the needs of the service. If it can be launched over a Local Area Network connection, specify a `lan-config`. Otherwise, at minimum, a `tor-config` must be specified. +interfaces: + # This key is the internal name that the OS will use to configure the interface + main: + # A human readable name for display in the UI + name: User Interface + # A descriptive description of what the interface does + description: A simple user interface that is expected to display the text "Hello Word" + tor-config: + # Port mappings are from the external port to the internal container port + port-mapping: + 80: "80" + # Port mappings are from the external port to the internal container port + lan-config: + 443: + ssl: true + internal: 80 + # Denotes if the service has a user interface to display + ui: true + # Denotes the protocol specifications used by this interface + protocols: + - tcp + - http +dependencies: {} +# Specifies how backups should be run for this service. The default StartOS provided option is to use the duplicity backup library on a system image (compat) +backup: + create: + # Currently, only docker actions are supported. + type: docker + # The docker image to use. In this case, a pre-loaded system image called compat + image: compat + # Required if the action uses a system image. The default value is false. + system: true + # The executable to run the command to begin the backup create process + entrypoint: compat + # Arguments to pass into the entrypoint executable. In this example, the full command run will be: `compat duplicity hello-world /mnt/backup /root/data` + args: + - duplicity + - create + - /mnt/backup + # For duplicity, the backup mount point needs to be something other than `/root`, so we default to `/root/data` + - /root/data + mounts: + # BACKUP is the default volume that is used for backups. This is whatever backup drive is mounted to the device, or a network filesystem. + # The value here donates where the mount point will be. The backup drive is mounted to this location. + BACKUP: "/mnt/backup" + main: "/root/data" + # The action to execute the backup restore functionality. Details for the keys below are the same as above. + restore: + type: docker + image: compat + system: true + entrypoint: compat + args: + - duplicity + - restore + - /mnt/backup + - /root/data + mounts: + BACKUP: "/mnt/backup" + main: "/root/data" +migrations: + from: + "*": + type: script + args: ["from"] + to: + "*": + type: script + args: ["to"] diff --git a/scripts/deps.ts b/scripts/deps.ts new file mode 100644 index 0000000..3105b54 --- /dev/null +++ b/scripts/deps.ts @@ -0,0 +1 @@ +export * from "https://deno.land/x/embassyd_sdk@v0.3.3.0.11/mod.ts"; diff --git a/scripts/embassy.ts b/scripts/embassy.ts new file mode 100644 index 0000000..8d2f468 --- /dev/null +++ b/scripts/embassy.ts @@ -0,0 +1,5 @@ +export { setConfig } from "./procedures/setConfig.ts"; +export { getConfig } from "./procedures/getConfig.ts"; +export { properties } from "./procedures/properties.ts"; +export { migration } from "./procedures/migrations.ts"; +export { health } from "./procedures/healthChecks.ts"; diff --git a/scripts/procedures/getConfig.ts b/scripts/procedures/getConfig.ts new file mode 100644 index 0000000..2948d64 --- /dev/null +++ b/scripts/procedures/getConfig.ts @@ -0,0 +1,5 @@ +// To utilize the default config system built, this file is required. It defines the *structure* of the configuration file. These structured options display as changeable UI elements within the "Config" section of the service details page in the StartOS UI. + +import { compat, types as T } from "../deps.ts"; + +export const getConfig: T.ExpectedExports.getConfig = compat.getConfig({}); diff --git a/scripts/procedures/healthChecks.ts b/scripts/procedures/healthChecks.ts new file mode 100644 index 0000000..c21d4ee --- /dev/null +++ b/scripts/procedures/healthChecks.ts @@ -0,0 +1,7 @@ +import { types as T, healthUtil } from "../deps.ts"; + +export const health: T.ExpectedExports.health = { + async "web-ui"(effects, duration) { + return healthUtil.checkWebUrl("http://hello-world.embassy:80")(effects, duration).catch(healthUtil.catchError(effects)) + }, +}; diff --git a/scripts/procedures/migrations.ts b/scripts/procedures/migrations.ts new file mode 100644 index 0000000..fe4371e --- /dev/null +++ b/scripts/procedures/migrations.ts @@ -0,0 +1,4 @@ +import { compat, types as T } from "../deps.ts"; + +export const migration: T.ExpectedExports.migration = compat.migrations + .fromMapping({}, "0.3.5" ); diff --git a/scripts/procedures/properties.ts b/scripts/procedures/properties.ts new file mode 100644 index 0000000..dff99aa --- /dev/null +++ b/scripts/procedures/properties.ts @@ -0,0 +1,3 @@ +import { compat, types as T } from "../deps.ts"; + +export const properties: T.ExpectedExports.properties = compat.properties; diff --git a/scripts/procedures/setConfig.ts b/scripts/procedures/setConfig.ts new file mode 100644 index 0000000..43d308f --- /dev/null +++ b/scripts/procedures/setConfig.ts @@ -0,0 +1,5 @@ +// This is where any configuration rules related to the configuration would go. These ensure that the user can only create a valid config. + +import { compat, } from "../deps.ts"; + +export const setConfig = compat.setConfig;