-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add go init profile and go-extension (#631)
* WIP go-extension * fix linting * Fix linting * fix linting * Fix linting * typo fix * Rename init profile go to go-extension * Add working dir * Replace go-extention base directory /go with /app * Run github action * Preparing init profile * fix pebble link * Revert spread.yaml This reverts commit 7030c28. * build-base not needed in the test anymore * Reorder and improve code * Fix formatting * Add 24.04 base to the go extension as default * Revert spread.yaml file This reverts commit 956e2ce. * Add base 24.04 by default instead of 22.04 * Add asser for docs_url in exception * Add extra platforms for flask, django and go * Rerun actions * Launch actions * launch actions * Update rockcraft/commands/init.py apply suggested fix to docs Co-authored-by: Tiago Nobrega <[email protected]> * Update rockcraft/commands/init.py Co-authored-by: Tiago Nobrega <[email protected]> * Update rockcraft/extensions/go.py Co-authored-by: Tiago Nobrega <[email protected]> * doc_url -> doc_slug * Improve _get_nested, as "." is a valid character in a yaml name * correct type to dict * remove pdb * remove support for 22.04 in go * comment non amd64 platforms in extensions * Add comment for platforms in extensions * add missing type annotations * Apply suggestions from code review Co-authored-by: Alex Lowe <[email protected]> --------- Co-authored-by: Tiago Nobrega <[email protected]> Co-authored-by: Alex Lowe <[email protected]>
- Loading branch information
1 parent
b7d7059
commit 136c0a7
Showing
9 changed files
with
601 additions
and
3 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 |
---|---|---|
|
@@ -98,8 +98,13 @@ class InitCommand(AppCommand): | |
most important story about it. Keep it under 100 words though, | ||
we live in tweetspace and your description wants to look good in the | ||
container registries out there. | ||
platforms: # the platforms this rock should be built on and run on | ||
# the platforms this rock should be built on and run on. | ||
# you can check your architecture with `dpkg --print-architecture` | ||
platforms: | ||
amd64: | ||
# arm64: | ||
# ppc64el: | ||
# s390x: | ||
# to ensure the flask-framework extension works properly, your Flask application | ||
# should have an `app.py` file with an `app` object as the WSGI entrypoint. | ||
|
@@ -171,8 +176,13 @@ class InitCommand(AppCommand): | |
most important story about it. Keep it under 100 words though, | ||
we live in tweetspace and your description wants to look good in the | ||
container registries out there. | ||
platforms: # The platforms this rock should be built on and run on | ||
# the platforms this rock should be built on and run on. | ||
# you can check your architecture with `dpkg --print-architecture` | ||
platforms: | ||
amd64: | ||
# arm64: | ||
# ppc64el: | ||
# s390x: | ||
# to ensure the django-framework extension functions properly, your Django project | ||
# should have a structure similar to the following with ./{snake_name}/{snake_name}/wsgi.py | ||
|
@@ -201,6 +211,71 @@ class InitCommand(AppCommand): | |
""" | ||
) | ||
), | ||
"go-framework": _InitProfile( | ||
rockcraft_yaml=textwrap.dedent( | ||
"""\ | ||
name: {name} | ||
# see {versioned_url}/explanation/bases/ | ||
# for more information about bases and using 'bare' bases for chiselled rocks | ||
base: bare # as an alternative, a ubuntu base can be used | ||
build-base: [email protected] # build-base is required when the base is bare | ||
version: '0.1' # just for humans. Semantic versioning is recommended | ||
summary: A summary of your Go application # 79 char long summary | ||
description: | | ||
This is {name}'s description. You have a paragraph or two to tell the | ||
most important story about it. Keep it under 100 words though, | ||
we live in tweetspace and your description wants to look good in the | ||
container registries out there. | ||
# the platforms this rock should be built on and run on. | ||
# you can check your architecture with `dpkg --print-architecture` | ||
platforms: | ||
amd64: | ||
# arm64: | ||
# ppc64el: | ||
# s390x: | ||
# to ensure the go-framework extension functions properly, your Go project | ||
# should have a go.mod file. Check the parts section for the selection of | ||
# the default binary. | ||
# see {versioned_url}/reference/extensions/go-framework | ||
# for more information. | ||
# +-- {snake_name} | ||
# | |-- go.mod | ||
# | |-- migrate.sh | ||
extensions: | ||
- go-framework | ||
# uncomment the sections you need and adjust according to your requirements. | ||
# parts: | ||
# go-framework/install-app: | ||
# # select a specific Go version. Otherwise the current stable one will be used. | ||
# build-snaps: | ||
# - go/1.22/stable | ||
# organize: | ||
# # if the main package is in the base directory and the rockcraft name | ||
# # attribute is equal to the go module name, the name of the server will | ||
# # be selected correctly, otherwise you can adjust it. | ||
# # the file in /usr/local/bin/ with the name of the rockcraft project will be | ||
# # the binary to run your server. | ||
# # you can also include here other binary files to be included in the rock. | ||
# bin/otherbinary: usr/local/bin/projectname | ||
# go-framework/assets: | ||
# stage: | ||
# # by default, only the files in templates/ and static/ | ||
# # are copied into the image. You can modify the list below to override | ||
# # the default list and include or exclude specific files/directories | ||
# # in your project. | ||
# # note: Prefix each entry with "app/" followed by the local path. | ||
# - app/templates | ||
# - app/static | ||
# - app/otherdirectory | ||
# - app/otherfile | ||
""" | ||
) | ||
), | ||
} | ||
_DEFAULT_PROFILE = "simple" | ||
|
||
|
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
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,214 @@ | ||
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
# | ||
# Copyright 2024 Canonical Ltd. | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License version 3 as | ||
# published by the Free Software Foundation. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
"""An extension for Go based applications.""" | ||
|
||
import os | ||
import re | ||
from typing import Any, Dict, Tuple | ||
|
||
from overrides import override | ||
|
||
from ..errors import ExtensionError | ||
from .extension import Extension | ||
|
||
|
||
class GoFramework(Extension): | ||
"""An extension class for Go applications.""" | ||
|
||
@staticmethod | ||
@override | ||
def get_supported_bases() -> Tuple[str, ...]: | ||
"""Return supported bases.""" | ||
return "bare", "[email protected]" | ||
|
||
@staticmethod | ||
@override | ||
def is_experimental(base: str | None) -> bool: | ||
"""Check if the extension is in an experimental state.""" | ||
return True | ||
|
||
@override | ||
def get_root_snippet(self) -> dict[str, Any]: | ||
"""Return the root snippet to apply.""" | ||
self._check_project() | ||
|
||
snippet: Dict[str, Any] = { | ||
"run_user": "_daemon_", | ||
"services": { | ||
"go": { | ||
"override": "replace", | ||
"startup": "enabled", | ||
"command": self.project_name, | ||
"user": "_daemon_", | ||
"working-dir": "/app", | ||
}, | ||
}, | ||
} | ||
|
||
snippet["parts"] = { | ||
# This is needed in case there is no assets part, as the working directory is /app | ||
"go-framework/base-layout": { | ||
"plugin": "nil", | ||
"override-build": "mkdir -p ${CRAFT_PART_INSTALL}/app", | ||
}, | ||
"go-framework/install-app": self._get_install_app_part(), | ||
"go-framework/runtime": { | ||
"plugin": "nil", | ||
"stage-packages": ["ca-certificates_data"], | ||
}, | ||
} | ||
|
||
assets_part = self._get_install_assets_part() | ||
if assets_part: | ||
snippet["parts"]["go-framework/assets"] = assets_part | ||
|
||
return snippet | ||
|
||
@override | ||
def get_parts_snippet(self) -> dict[str, Any]: | ||
"""Return the parts to add to parts.""" | ||
return {} | ||
|
||
@override | ||
def get_part_snippet(self) -> dict[str, Any]: | ||
"""Return the part snippet to apply to existing parts.""" | ||
return {} | ||
|
||
@property | ||
def project_name(self) -> str: | ||
"""Return the normalized name of the rockcraft project.""" | ||
return self.yaml_data["name"] | ||
|
||
def _check_project(self) -> None: | ||
"""Check go.mod file exist in project.""" | ||
if not (self.project_root / "go.mod").exists(): | ||
raise ExtensionError( | ||
"missing go.mod file", | ||
doc_slug="/reference/extensions/go-framework", | ||
logpath_report=False, | ||
) | ||
|
||
def _get_install_app_part(self) -> Dict[str, Any]: | ||
"""Generate install-app part with the Go plugin.""" | ||
install_app = self._get_nested( | ||
self.yaml_data, ["parts", "go-framework/install-app"] | ||
) | ||
|
||
build_environment = install_app.get("build-environment", []) | ||
if self.yaml_data["base"] == "bare": | ||
for env_var in build_environment: | ||
if "CGO_ENABLED" in env_var: | ||
break | ||
else: | ||
build_environment = [{"CGO_ENABLED": "0"}] | ||
|
||
organize = install_app.get("organize", {}) | ||
binary_path = f"usr/local/bin/{self.project_name}" | ||
for path in organize.values(): | ||
if path == binary_path: | ||
break | ||
else: | ||
if not self._get_nested(self.yaml_data, ["services", "go", "command"]): | ||
organize[f"bin/{self.project_name}"] = binary_path | ||
|
||
install_app_part = { | ||
"plugin": "go", | ||
"source": ".", | ||
"organize": organize, | ||
} | ||
|
||
if not self._check_go_overriden(): | ||
build_snaps = install_app.get("build-snaps", []) | ||
build_snaps.append("go") | ||
install_app_part["build-snaps"] = build_snaps | ||
|
||
install_app_part["stage"] = list(organize.values()) | ||
if build_environment: | ||
install_app_part["build-environment"] = build_environment | ||
|
||
return install_app_part | ||
|
||
def _check_go_overriden(self) -> bool: | ||
"""Check if the user overrode the go snap or package for the build step.""" | ||
install_app = self._get_nested( | ||
self.yaml_data, ["parts", "go-framework/install-app"] | ||
) | ||
build_snaps = install_app.get("build-snaps", []) | ||
if build_snaps: | ||
for snap in build_snaps: | ||
if snap.startswith("go"): | ||
return True | ||
build_packages = install_app.get("build-packages", []) | ||
if build_packages: | ||
for package in build_packages: | ||
if package in ["gccgo-go", "golang-go"]: | ||
return True | ||
return False | ||
|
||
def _get_install_assets_part(self) -> Dict[str, Any] | None: | ||
"""Generate assets-stage part for extra assets in the project.""" | ||
# if stage is not in exclude mode, use it to generate organize | ||
if ( | ||
self._assets_stage | ||
and self._assets_stage[0] | ||
and self._assets_stage[0][0] != "-" | ||
): | ||
renaming_map = { | ||
os.path.relpath(file, "app"): file for file in self._assets_stage | ||
} | ||
else: | ||
return None | ||
|
||
return { | ||
"plugin": "dump", | ||
"source": ".", | ||
"organize": renaming_map, | ||
"stage": self._assets_stage, | ||
} | ||
|
||
@property | ||
def _assets_stage(self) -> list[str]: | ||
"""Return the assets stage list for the Go project.""" | ||
user_stage = self._get_nested( | ||
self.yaml_data, ["parts", "go-framework/assets"] | ||
).get("stage", []) | ||
|
||
if not all(re.match("-? *app/", p) for p in user_stage): | ||
raise ExtensionError( | ||
"go-framework extension requires the 'stage' entry in the " | ||
"go-framework/assets part to start with app", | ||
doc_slug="/reference/extensions/go-framework", | ||
logpath_report=False, | ||
) | ||
if not user_stage: | ||
user_stage = [ | ||
f"app/{f}" | ||
for f in ( | ||
"migrate", | ||
"migrate.sh", | ||
"static", | ||
"templates", | ||
) | ||
if (self.project_root / f).exists() | ||
] | ||
return user_stage | ||
|
||
def _get_nested(self, obj: dict, paths: list[str]) -> dict: | ||
"""Get a nested object using a path (a list of keys).""" | ||
for key in paths: | ||
obj = obj.get(key, {}) | ||
return obj |
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 @@ | ||
test |
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,3 @@ | ||
module github.com/canonical/NAME | ||
|
||
go 1.22.4 |
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,15 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
) | ||
|
||
func hello(w http.ResponseWriter, req *http.Request) { | ||
fmt.Fprintf(w, "ok") | ||
} | ||
|
||
func main() { | ||
http.HandleFunc("/", hello) | ||
http.ListenAndServe(":8000", nil) | ||
} |
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,46 @@ | ||
summary: go extension test | ||
environment: | ||
SCENARIO/bare: bare | ||
SCENARIO/base_2404: ubuntu-24.04 | ||
ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "true" | ||
|
||
execute: | | ||
NAME="go-${SCENARIO//./-}" | ||
ROCK_FILE="${NAME}_0.1_amd64.rock" | ||
IMAGE="${NAME}:0.1" | ||
run_rockcraft init --name "${NAME}" --profile go-framework | ||
sed -i "s/^name: .*/name: ${NAME}/g" rockcraft.yaml | ||
sed -i "s/^base: .*/base: ${SCENARIO//-/@}/g" rockcraft.yaml | ||
if [ "${SCENARIO}" != "bare" ]; then | ||
sed -i "s/^build-base: .*/build-base: ${SCENARIO//-/@}/g" rockcraft.yaml | ||
fi | ||
sed -i "s/NAME/${NAME}/g" go.mod | ||
run_rockcraft pack | ||
test -f "${ROCK_FILE}" | ||
test ! -d work | ||
# Ensure docker does not have this container image | ||
docker rmi --force "${IMAGE}" | ||
# Install container | ||
sudo rockcraft.skopeo --insecure-policy copy "oci-archive:${ROCK_FILE}" "docker-daemon:${IMAGE}" | ||
# Ensure container exists | ||
docker images "${IMAGE}" | MATCH "${NAME}" | ||
# ensure container doesn't exist | ||
docker rm -f "${NAME}-container" | ||
# test the default go service | ||
docker run --name "${NAME}-container" -d -p 8137:8000 "${IMAGE}" | ||
retry -n 5 --wait 2 curl localhost:8137 | ||
[ "$(curl -sSf localhost:8137)" == "ok" ] | ||
restore: | | ||
NAME="go-${SCENARIO//./-}" | ||
sed -i "s/${NAME}/NAME/g" go.mod | ||
docker stop "${NAME}-container" | ||
docker rm "${NAME}-container" | ||
rm -f "*.rock" rockcraft.yaml | ||
docker system prune -a -f |
Oops, something went wrong.