From 0572cfd08e497ab48a756b1c05386682d6396ffb Mon Sep 17 00:00:00 2001 From: Filipe Bojikian Rissi Date: Tue, 25 Aug 2026 23:22:31 -0300 Subject: [PATCH] feat: add java presets and api actions --- README.md | 1 + core/automate/actions.go | 26 ++++ core/automate/actions_test.go | 43 +++++ core/automate/executor.go | 208 +++++++++++++++++++++++++ core/shell/fake_prompt_select.go | 10 ++ core/shell/prompt_select.go | 23 ++- docs/03-Presets/0-About-Presets.md | 5 + docs/03-Presets/Java.md | 17 ++ docs/03-Presets/SpringBoot.md | 17 ++ go.mod | 3 + go.sum | 3 + presets/java/config.yml | 19 +++ presets/java/docker-compose.yml | 18 +++ presets/java/kool.yml | 5 + presets/spring-boot/config.yml | 19 +++ presets/spring-boot/docker-compose.yml | 18 +++ presets/spring-boot/kool.yml | 5 + recipes/create-java.yml | 7 + recipes/create-spring-boot.yml | 28 ++++ recipes/java-h2.yml | 5 + recipes/java-postgresql-16.yml | 7 + recipes/java-sqlite.yml | 5 + recipes/pick-java-cache.yml | 13 ++ recipes/pick-java-db.yml | 25 +++ templates/database/postgresql16.yml | 19 +++ 25 files changed, 548 insertions(+), 1 deletion(-) create mode 100644 docs/03-Presets/Java.md create mode 100644 docs/03-Presets/SpringBoot.md create mode 100644 presets/java/config.yml create mode 100644 presets/java/docker-compose.yml create mode 100644 presets/java/kool.yml create mode 100644 presets/spring-boot/config.yml create mode 100644 presets/spring-boot/docker-compose.yml create mode 100644 presets/spring-boot/kool.yml create mode 100644 recipes/create-java.yml create mode 100644 recipes/create-spring-boot.yml create mode 100644 recipes/java-h2.yml create mode 100644 recipes/java-postgresql-16.yml create mode 100644 recipes/java-sqlite.yml create mode 100644 recipes/pick-java-cache.yml create mode 100644 recipes/pick-java-db.yml create mode 100644 templates/database/postgresql16.yml diff --git a/README.md b/README.md index d6894983..df1ecd98 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ To help you start building real-world applications, we've created Kool Presets a - **Node**: [NestJS](docs/03-Presets/NestJS.md), [AdonisJs](docs/03-Presets/AdonisJs.md), [Express.js](/docs/03-Presets/ExpressJS.md) - **PHP**: [Laravel](docs/03-Presets/Laravel.md), [Laravel Octane](docs/03-Presets/Laravel+Octane.md), [Symfony](docs/03-Presets/Symfony.md), [CodeIgniter](docs/03-Presets/CodeIgniter.md) - **Javascript**: [Next.js](docs/03-Presets/NextJS.md), [NuxtJS](docs/03-Presets/NuxtJS.md) +- **Java**: [Java with Gradle](docs/03-Presets/Java.md), [Spring Boot](docs/03-Presets/SpringBoot.md) - **Others**: [Hugo](docs/03-Presets/Hugo.md), [WordPress](docs/03-Presets/WordPress.md) #### Monorepo structures diff --git a/core/automate/actions.go b/core/automate/actions.go index 0e2c57a2..3af4506d 100644 --- a/core/automate/actions.go +++ b/core/automate/actions.go @@ -9,6 +9,8 @@ const ( TypePrompt TypeRecipe TypeMerge + TypeInput + TypeAPI ) // ActionSet represents a set of single actions or a question @@ -34,8 +36,24 @@ type Action struct { Scripts []string `yaml:"scripts"` // prompt Prompt string `yaml:"prompt"` + Input string `yaml:"input"` Default string `yaml:"default"` Options []*ActionSet `yaml:"options"` + // api + API string `yaml:"api"` + Prompts []*APIField `yaml:"prompts"` +} + +// APIField describes a prompt backed by a field in an API response. +type APIField struct { + Path string `yaml:"path"` + Options string `yaml:"options"` + Value string `yaml:"value"` + Label string `yaml:"label"` + Default string `yaml:"default"` + Prompt string `yaml:"prompt"` + Ref string `yaml:"ref"` + Multiple bool `yaml:"multiple"` } // Type tells the actual implementation of this action @@ -56,9 +74,17 @@ func (a *Action) Type() ActionType { return TypePrompt } + if a.Input != "" { + return TypeInput + } + if a.Merge != "" { return TypeMerge } + if a.API != "" { + return TypeAPI + } + return TypeUnknown } diff --git a/core/automate/actions_test.go b/core/automate/actions_test.go index 6ee9d6cf..0d12df33 100644 --- a/core/automate/actions_test.go +++ b/core/automate/actions_test.go @@ -70,6 +70,49 @@ func TestParseActionPrompt(t *testing.T) { }) } +func TestParseActionInput(t *testing.T) { + a := parseAction("input: 'Project name'\nref: 'PROJECT_NAME'\ndefault: 'application'", t) + + if a.Input != "Project name" || a.Ref != "PROJECT_NAME" || a.Default != "application" { + t.Errorf("failed parsing ActionInput: %v", a) + } + if a.Type() != TypeInput { + t.Errorf("failed parsing ActionInput type; got: %v", a.Type()) + } +} + +func TestParseActionAPI(t *testing.T) { + a := parseAction("api: 'https://example.test/options'\nprompts:\n - path: 'data'\n options: 'items'\n value: 'key'\n label: 'title'\n multiple: true", t) + + if a.API == "" || len(a.Prompts) != 1 || a.Prompts[0].Options != "items" || !a.Prompts[0].Multiple { + t.Errorf("failed parsing ActionAPI: %v", a) + } + if a.Type() != TypeAPI { + t.Errorf("failed parsing ActionAPI type; got: %v", a.Type()) + } +} + +func TestAPIOptions(t *testing.T) { + data := map[string]any{ + "data": map[string]any{ + "default": "two", + "items": []any{ + map[string]any{"key": "one", "title": "One"}, + map[string]any{"key": "two", "title": "Two"}, + }, + }, + } + options, defaultValue, err := apiOptions(data, &APIField{ + Path: "data", + Options: "items", + Value: "key", + Label: "title", + }) + if err != nil || len(options) != 2 || options[1].Value != "two" || defaultValue != "two" { + t.Errorf("unexpected API options: options=%v default=%q err=%v", options, defaultValue, err) + } +} + func TestParseActionMerge(t *testing.T) { t.Run("Parse merge basic", func(t *testing.T) { a := parseAction("merge: 'foo'", t) diff --git a/core/automate/executor.go b/core/automate/executor.go index fb39786d..5586f9d6 100644 --- a/core/automate/executor.go +++ b/core/automate/executor.go @@ -1,12 +1,15 @@ package automate import ( + "encoding/json" "fmt" "io" "kool-dev/kool/core/builder" "kool-dev/kool/core/shell" "kool-dev/kool/services/yamler" + "net/http" "os" + "strings" "time" "github.com/spf13/afero" @@ -21,6 +24,8 @@ type Executor struct { getFromSource RetrieveSource local afero.Fs prompter shell.PromptSelect + multiPrompter shell.PromptMultiSelect + input shell.PromptInput // promptState is a map of prompt answers promptState map[string]string @@ -32,6 +37,8 @@ func NewExecutor(sh shell.Shell, fn RetrieveSource) *Executor { getFromSource: fn, local: afero.NewOsFs(), prompter: shell.NewPromptSelect(), + multiPrompter: shell.NewPromptMultiSelect(), + input: shell.NewPromptInput(), promptState: make(map[string]string), } } @@ -71,6 +78,14 @@ func (e *Executor) Do(steps []*ActionSet) (err error) { if err = e.prompt(action); err != nil { return } + case TypeInput: + if err = e.inputValue(action); err != nil { + return + } + case TypeAPI: + if err = e.api(action); err != nil { + return + } default: err = fmt.Errorf("ops, something is wrong with this preset config (%d)", action.Type()) return @@ -220,6 +235,199 @@ func (e *Executor) prompt(action *Action) (err error) { return } +func (e *Executor) inputValue(action *Action) (err error) { + value, err := e.input.Input(action.Input, action.Default) + if err != nil { + return + } + if action.Ref != "" { + e.promptState[action.Ref] = value + } + + if action.Ref != "" { + err = os.Setenv(action.Ref, value) + } + return +} + +func (e *Executor) api(action *Action) (err error) { + var ( + response *http.Response + data map[string]any + ) + + client := &http.Client{Timeout: 15 * time.Second} + if response, err = client.Get(action.API); err != nil { + return + } + defer func() { + if closeErr := response.Body.Close(); err == nil { + err = closeErr + } + }() + + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { + return fmt.Errorf("API request %s returned HTTP %d", action.API, response.StatusCode) + } + if err = json.NewDecoder(response.Body).Decode(&data); err != nil { + return fmt.Errorf("could not decode API response from %s: %w", action.API, err) + } + + for _, field := range action.Prompts { + var ( + values []apiOption + defaultValue string + ) + if values, defaultValue, err = apiOptions(data, field); err != nil { + return fmt.Errorf("could not read API field %q: %w", field.Path, err) + } + if len(values) == 0 { + return fmt.Errorf("API field %q did not contain any options", field.Path) + } + + options := make([]string, 0, len(values)) + byLabel := make(map[string]string, len(values)) + for _, option := range values { + options = append(options, option.Label) + byLabel[option.Label] = option.Value + } + if defaultValue != "" { + defaultLabel := "" + for _, option := range values { + if option.Value == defaultValue { + defaultLabel = option.Label + break + } + } + if defaultLabel != "" { + for i, option := range options { + if option == defaultLabel { + options = append([]string{defaultLabel}, append(options[:i], options[i+1:]...)...) + break + } + } + } + } + + if field.Multiple { + var selected []string + if selected, err = e.multiPrompter.AskMany(field.Prompt, options); err != nil { + return + } + ids := make([]string, 0, len(selected)) + for _, label := range selected { + if value, ok := byLabel[label]; ok { + ids = append(ids, value) + } + } + err = e.setAPIValue(field.Ref, strings.Join(ids, ",")) + continue + } + + var selected string + if selected, err = e.prompter.Ask(field.Prompt, options); err != nil { + return + } + err = e.setAPIValue(field.Ref, byLabel[selected]) + } + return +} + +type apiOption struct { + Label string + Value string +} + +func apiOptions(data map[string]any, field *APIField) (options []apiOption, defaultValue string, err error) { + var current any + if current, err = apiPath(data, field.Path); err != nil { + return nil, "", err + } + if object, ok := current.(map[string]any); ok { + defaultValue, _ = object["default"].(string) + if field.Options != "" { + current, err = apiPath(object, field.Options) + } else { + current = object["values"] + } + } else if field.Options != "" { + return nil, "", fmt.Errorf("options path %q requires an object at path %q", field.Options, field.Path) + } + if field.Default != "" { + defaultValue = field.Default + } + if err != nil { + return nil, "", err + } + values, ok := current.([]any) + if !ok { + return nil, "", fmt.Errorf("path %q has no options", field.Path) + } + options, err = flattenAPIOptions(values, "", field.Value, field.Label) + return +} + +func apiPath(root any, path string) (any, error) { + current := root + for _, part := range strings.Split(path, ".") { + if part == "" { + continue + } + value, ok := current.(map[string]any) + if !ok { + return nil, fmt.Errorf("options path %q is not an object", path) + } + current, ok = value[part] + if !ok { + return nil, fmt.Errorf("options path %q does not exist", path) + } + } + return current, nil +} + +func flattenAPIOptions(values []any, group, valueKey, labelKey string) ([]apiOption, error) { + var options []apiOption + for _, raw := range values { + object, ok := raw.(map[string]any) + if !ok { + return nil, fmt.Errorf("option is not an object") + } + name, _ := object["name"].(string) + id, _ := object["id"].(string) + if nested, ok := object["values"].([]any); ok { + if nestedOptions, err := flattenAPIOptions(nested, name, valueKey, labelKey); err != nil { + return nil, err + } else { + options = append(options, nestedOptions...) + } + continue + } + if valueKey != "" { + id, _ = object[valueKey].(string) + } + if labelKey != "" { + name, _ = object[labelKey].(string) + } + if id == "" || name == "" { + continue + } + label := name + if group != "" { + label = group + ": " + name + } + options = append(options, apiOption{Label: label, Value: id}) + } + return options, nil +} + +func (e *Executor) setAPIValue(ref, value string) error { + if ref == "" { + return nil + } + e.promptState[ref] = value + return os.Setenv(ref, value) +} + func (e *Executor) recipe(action *Action) (err error) { var ( set = new(ActionSet) diff --git a/core/shell/fake_prompt_select.go b/core/shell/fake_prompt_select.go index 08cb02dd..abb630ee 100644 --- a/core/shell/fake_prompt_select.go +++ b/core/shell/fake_prompt_select.go @@ -22,6 +22,16 @@ func (f *FakePromptSelect) Ask(question string, options []string) (answer string return } +// AskMany mocked behavior for testing multiple option prompts. +func (f *FakePromptSelect) AskMany(question string, options []string) (answers []string, err error) { + f.CalledAsk = true + if answer := f.MockAnswer[question]; answer != "" { + answers = []string{answer} + } + err = f.MockError[question] + return +} + // Confirm mocked behavior for testing prompting a confirm question func (f *FakePromptSelect) Confirm(question string, args ...any) (confirmed bool, err error) { f.CalledConfirm = append(f.CalledConfirm, &struct { diff --git a/core/shell/prompt_select.go b/core/shell/prompt_select.go index fbe3c756..bfb071ad 100644 --- a/core/shell/prompt_select.go +++ b/core/shell/prompt_select.go @@ -10,10 +10,14 @@ import ( // PromptSelect contract that holds logic for prompt a select question type PromptSelect interface { Ask(string, []string) (string, error) - Confirm(string, ...any) (bool, error) } +// PromptMultiSelect prompts the user to select multiple options. +type PromptMultiSelect interface { + AskMany(string, []string) ([]string, error) +} + // DefaultPromptSelect holds data for prompting a select question type DefaultPromptSelect struct{} @@ -22,6 +26,11 @@ func NewPromptSelect() PromptSelect { return &DefaultPromptSelect{} } +// NewPromptMultiSelect creates a multiple option prompt. +func NewPromptMultiSelect() PromptMultiSelect { + return &DefaultPromptSelect{} +} + // Ask prompt to the user a select question func (p *DefaultPromptSelect) Ask(question string, options []string) (answer string, err error) { prompt := &survey.Select{ @@ -34,6 +43,18 @@ func (p *DefaultPromptSelect) Ask(question string, options []string) (answer str return } +// AskMany prompts the user to select one or more options. +func (p *DefaultPromptSelect) AskMany(question string, options []string) (answers []string, err error) { + prompt := &survey.MultiSelect{ + Message: question, + Options: options, + } + if err = survey.AskOne(prompt, &answers); err != nil && err == terminal.InterruptErr { + err = ErrUserCancelled + } + return +} + // Confirm prompts to the user a Yes/No confirm question func (p *DefaultPromptSelect) Confirm(question string, args ...any) (confirmed bool, err error) { if args != nil { diff --git a/docs/03-Presets/0-About-Presets.md b/docs/03-Presets/0-About-Presets.md index 7e4b2c81..ce11bb4f 100644 --- a/docs/03-Presets/0-About-Presets.md +++ b/docs/03-Presets/0-About-Presets.md @@ -26,6 +26,11 @@ By leveraging Kool Presets, you not only reduce the learning curve associated wi - [Next.js](/docs/03-Presets/NextJS.md) - [NuxtJS](/docs/03-Presets/NuxtJS.md) +## Java + +- [Java with Gradle](/docs/03-Presets/Java.md) +- [Spring Boot](/docs/03-Presets/SpringBoot.md) + ## Others - [Hugo](/docs/03-Presets/Hugo.md) diff --git a/docs/03-Presets/Java.md b/docs/03-Presets/Java.md new file mode 100644 index 00000000..6e601b25 --- /dev/null +++ b/docs/03-Presets/Java.md @@ -0,0 +1,17 @@ +# Start a Java Project with Gradle + +Create a Java application using Gradle and Docker: + +```bash +kool create java my-java-app +``` + +The preset uses `gradle init` with Java 21, Kotlin build scripts, and JUnit Jupiter. It provides Docker-backed scripts for building, testing, and running the application. + +During setup, database and cache services are optional. Database choices include PostgreSQL, MySQL, MariaDB, MongoDB, H2, and SQLite. H2 and SQLite create local file-backed data directories under `data/` instead of starting a database container. Redis and Memcached are available as cache services. + +```bash +kool run build +kool run test +kool run run +``` diff --git a/docs/03-Presets/SpringBoot.md b/docs/03-Presets/SpringBoot.md new file mode 100644 index 00000000..238c5850 --- /dev/null +++ b/docs/03-Presets/SpringBoot.md @@ -0,0 +1,17 @@ +# Start a Spring Boot Project + +Create a Spring Boot application using the Spring Initializr API: + +```bash +kool create spring-boot my-spring-app +``` + +The wizard asks for the application coordinates, then fetches the current language, Java version, and dependency options from the Spring Initializr metadata API. This keeps the choices in sync with the provider instead of hardcoding them locally. + +After the project is generated, database and cache services are optional. Available databases include PostgreSQL, MySQL, MariaDB, MongoDB, H2, and SQLite. H2 and SQLite use local file-backed data directories under `data/`; Redis and Memcached are available as cache services. + +Run the generated application with: + +```bash +kool run run +``` diff --git a/go.mod b/go.mod index 7dc1dec8..c406538f 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require github.com/compose-spec/compose-go v1.20.2 require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/fatih/color v1.19.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v30 v30.1.0 // indirect @@ -45,10 +46,12 @@ require ( github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/tcnksm/go-gitconfig v0.1.2 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.52.0 // indirect ) diff --git a/go.sum b/go.sum index 02dd5b81..f378c147 100755 --- a/go.sum +++ b/go.sum @@ -16,6 +16,7 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/compose-spec/compose-go v1.20.2 h1:u/yfZHn4EaHGdidrZycWpxXgFffjYULlTbRfJ51ykjQ= github.com/compose-spec/compose-go v1.20.2/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -92,6 +93,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag= github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg= +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/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= @@ -114,6 +116,7 @@ github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= diff --git a/presets/java/config.yml b/presets/java/config.yml new file mode 100644 index 00000000..6c386730 --- /dev/null +++ b/presets/java/config.yml @@ -0,0 +1,19 @@ +tags: [ 'Java' ] + +name: 'Java with Gradle' + +create: + - name: Creating new Java Application + actions: + - recipe: create-java + +preset: + - name: 'Copy basic config files' + actions: + - copy: docker-compose.yml + - copy: kool.yml + + - name: 'Customize your setup' + actions: + - recipe: pick-java-db + - recipe: pick-java-cache diff --git a/presets/java/docker-compose.yml b/presets/java/docker-compose.yml new file mode 100644 index 00000000..778f3e8e --- /dev/null +++ b/presets/java/docker-compose.yml @@ -0,0 +1,18 @@ +services: + app: + image: kooldev/java:21 + command: ./gradlew run --no-daemon + environment: + GRADLE_USER_HOME: /home/gradle/.gradle + ports: + - "${KOOL_APP_PORT:-8080}:8080" + volumes: + - .:/home/gradle/project:delegated + - gradle_cache:/home/gradle/.gradle + working_dir: /home/gradle/project + networks: + - kool_local + - kool_global + +volumes: + gradle_cache: diff --git a/presets/java/kool.yml b/presets/java/kool.yml new file mode 100644 index 00000000..a8eacb95 --- /dev/null +++ b/presets/java/kool.yml @@ -0,0 +1,5 @@ +scripts: + gradle: kool docker kooldev/java:21 gradle + build: kool run gradle build --no-daemon + test: kool run gradle test --no-daemon + run: kool run gradle run --no-daemon diff --git a/presets/spring-boot/config.yml b/presets/spring-boot/config.yml new file mode 100644 index 00000000..d8e45574 --- /dev/null +++ b/presets/spring-boot/config.yml @@ -0,0 +1,19 @@ +tags: [ 'Java', 'Spring' ] + +name: 'Spring Boot' + +create: + - name: Creating new Spring Boot Application + actions: + - recipe: create-spring-boot + +preset: + - name: 'Copy basic config files' + actions: + - copy: docker-compose.yml + - copy: kool.yml + + - name: 'Customize your setup' + actions: + - recipe: pick-java-db + - recipe: pick-java-cache diff --git a/presets/spring-boot/docker-compose.yml b/presets/spring-boot/docker-compose.yml new file mode 100644 index 00000000..47ab19a1 --- /dev/null +++ b/presets/spring-boot/docker-compose.yml @@ -0,0 +1,18 @@ +services: + app: + image: kooldev/java:21 + command: ./gradlew bootRun --no-daemon + environment: + GRADLE_USER_HOME: /home/gradle/.gradle + ports: + - "${KOOL_APP_PORT:-8080}:8080" + volumes: + - .:/home/gradle/project:delegated + - gradle_cache:/home/gradle/.gradle + working_dir: /home/gradle/project + networks: + - kool_local + - kool_global + +volumes: + gradle_cache: diff --git a/presets/spring-boot/kool.yml b/presets/spring-boot/kool.yml new file mode 100644 index 00000000..5a2ef9ae --- /dev/null +++ b/presets/spring-boot/kool.yml @@ -0,0 +1,5 @@ +scripts: + gradle: kool docker kooldev/java:21 gradle + build: kool run gradle build --no-daemon + test: kool run gradle test --no-daemon + run: kool run gradle bootRun --no-daemon diff --git a/recipes/create-java.yml b/recipes/create-java.yml new file mode 100644 index 00000000..fbaaa933 --- /dev/null +++ b/recipes/create-java.yml @@ -0,0 +1,7 @@ +title: "Creating Java Application" + +actions: + - scripts: + - mkdir -p $CREATE_DIRECTORY + - docker pull -q gradle:8-jdk21 + - kool docker gradle:8-jdk21 bash -c "cd $CREATE_DIRECTORY && gradle init --no-daemon --type java-application --project-name $CREATE_DIRECTORY --dsl kotlin --test-framework junit-jupiter" diff --git a/recipes/create-spring-boot.yml b/recipes/create-spring-boot.yml new file mode 100644 index 00000000..460e44f2 --- /dev/null +++ b/recipes/create-spring-boot.yml @@ -0,0 +1,28 @@ +title: "Creating Spring Boot Application" + +actions: + - input: 'Group' + ref: 'SPRING_GROUP' + default: 'com.example' + - input: 'Artifact' + ref: 'SPRING_ARTIFACT' + default: 'application' + - input: 'Application name' + ref: 'SPRING_NAME' + default: 'Application' + - api: 'https://start.spring.io/metadata/client' + prompts: + - path: 'language' + prompt: 'Which language do you want to use?' + ref: 'SPRING_LANGUAGE' + - path: 'javaVersion' + prompt: 'Which Java version do you want to use?' + ref: 'SPRING_JAVA_VERSION' + - path: 'dependencies' + prompt: 'Which Spring dependencies do you want to use?' + ref: 'SPRING_DEPENDENCIES' + multiple: true + - scripts: + - docker pull -q gradle:8-jdk21 + - mkdir -p $CREATE_DIRECTORY + - kool docker gradle:8-jdk21 bash -c "cd $CREATE_DIRECTORY && curl -fsSLG https://start.spring.io/starter.zip --data-urlencode type=gradle-project --data-urlencode language=$SPRING_LANGUAGE --data-urlencode groupId=$SPRING_GROUP --data-urlencode artifactId=$SPRING_ARTIFACT --data-urlencode name=$SPRING_NAME --data-urlencode javaVersion=$SPRING_JAVA_VERSION --data-urlencode dependencies=$SPRING_DEPENDENCIES -o /tmp/starter.zip && unzip -q /tmp/starter.zip -d ." diff --git a/recipes/java-h2.yml b/recipes/java-h2.yml new file mode 100644 index 00000000..9eb4f287 --- /dev/null +++ b/recipes/java-h2.yml @@ -0,0 +1,5 @@ +title: "H2 file-based database" + +actions: + - scripts: + - mkdir -p data/h2 diff --git a/recipes/java-postgresql-16.yml b/recipes/java-postgresql-16.yml new file mode 100644 index 00000000..d2fba3dc --- /dev/null +++ b/recipes/java-postgresql-16.yml @@ -0,0 +1,7 @@ +title: "PostgreSQL 16" + +actions: + - merge: database/postgresql16.yml + dst: docker-compose.yml + - merge: scripts/postgresql.yml + dst: kool.yml diff --git a/recipes/java-sqlite.yml b/recipes/java-sqlite.yml new file mode 100644 index 00000000..3e77a571 --- /dev/null +++ b/recipes/java-sqlite.yml @@ -0,0 +1,5 @@ +title: "SQLite file-based database" + +actions: + - scripts: + - mkdir -p data/sqlite diff --git a/recipes/pick-java-cache.yml b/recipes/pick-java-cache.yml new file mode 100644 index 00000000..c7c8350b --- /dev/null +++ b/recipes/pick-java-cache.yml @@ -0,0 +1,13 @@ +title: "Wizard: Java cache service" + +actions: + - prompt: 'Which cache service do you want to use?' + default: 'None - do not use a cache' + options: + - name: 'None - do not use a cache' + - name: 'Redis 7.0' + actions: + - recipe: redis-7 + - name: 'Memcached 1.6' + actions: + - recipe: memcached-1.6 diff --git a/recipes/pick-java-db.yml b/recipes/pick-java-db.yml new file mode 100644 index 00000000..6818604d --- /dev/null +++ b/recipes/pick-java-db.yml @@ -0,0 +1,25 @@ +title: "Wizard: Java database" + +actions: + - prompt: 'Which database service do you want to use?' + default: 'None - do not use a database' + options: + - name: 'None - do not use a database' + - name: 'PostgreSQL 16' + actions: + - recipe: java-postgresql-16 + - name: 'MySQL 8.0' + actions: + - recipe: mysql-8 + - name: 'MariaDB 10.5' + actions: + - recipe: maria-10.5 + - name: 'MongoDB' + actions: + - recipe: mongodb + - name: 'H2 (file-based)' + actions: + - recipe: java-h2 + - name: 'SQLite (file-based)' + actions: + - recipe: java-sqlite diff --git a/templates/database/postgresql16.yml b/templates/database/postgresql16.yml new file mode 100644 index 00000000..1663014c --- /dev/null +++ b/templates/database/postgresql16.yml @@ -0,0 +1,19 @@ +services: + database: + image: postgres:16-alpine + ports: + - "${KOOL_DATABASE_PORT:-5432}:5432" + environment: + POSTGRES_DB: "${DB_DATABASE-database}" + POSTGRES_USER: "${DB_USERNAME-user}" + POSTGRES_PASSWORD: "${DB_PASSWORD-pass}" + POSTGRES_HOST_AUTH_METHOD: "trust" + volumes: + - database:/var/lib/postgresql/data:delegated + networks: + - kool_local + healthcheck: + test: ["CMD-SHELL", "pg_isready -q -d ${DB_DATABASE-database} -U ${DB_USERNAME-user}"] + +volumes: + database: