Skip to content

Commit

Permalink
Implement Webservice Packer plugin (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
QubitPi authored Jul 3, 2024
1 parent ef59654 commit ad2a0a4
Show file tree
Hide file tree
Showing 10 changed files with 422 additions and 11 deletions.
85 changes: 85 additions & 0 deletions docs/provisioners/webservice-repository.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
Type: `webservice`

<!--
Include a short description about the provisioner. This is a good place
to call out what the provisioner does, and any additional text that might
be helpful to a user. See https://www.packer.io/docs/provisioners/null
-->

The `webservice` provisioner is used to install Jersey-Jetty webservice WAR file in AWS AMI image


<!-- Provisioner Configuration Fields -->

**Required**

- `warSource` (string) - The path to a local WAR file to upload to the machine. The path can be absolute or relative. If
it is relative, it is relative to the working directory when Packer is executed.


<!--
Optional Configuration Fields

Configuration options that are not required or have reasonable defaults
should be listed under the optionals section. Defaults values should be
noted in the description of the field
-->

**Optional**

- `homeDir` (string) - The `$Home` directory in AMI image; default to `/home/ubuntu`

<!--
A basic example on the usage of the provisioner. Multiple examples
can be provided to highlight various configurations.

-->

### Example Usage

```hcl
packer {
required_plugins {
amazon = {
version = ">= 0.0.2"
source = "github.com/hashicorp/amazon"
}
}
}

source "amazon-ebs" "hashicorp-aws" {
ami_name = "packer-plugin-hashicorp-aws-acc-test-ami"
force_deregister = "true"
force_delete_snapshot = "true"

instance_type = "t2.micro"
launch_block_device_mappings {
device_name = "/dev/sda1"
volume_size = 8
volume_type = "gp2"
delete_on_termination = true
}
region = "us-west-1"
source_ami_filter {
filters = {
name = "ubuntu/images/*ubuntu-*-22.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["099720109477"]
}
ssh_username = "ubuntu"
}

build {
sources = [
"source.amazon-ebs.hashicorp-aws"
]

provisioner "hashicorp-aws-webservice-provisioner" {
homeDir = "/home/ubuntu"
warSource = "my-webservice.war"
}
}
```
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package main
import (
"fmt"
sonatypeNexusRepository "github.com/QubitPi/packer-plugin-hashicorp-aws/provisioner/sonatype-nexus-repository"
"github.com/QubitPi/packer-plugin-hashicorp-aws/provisioner/webservice"
"os"

dockerMailServerProv "github.com/QubitPi/packer-plugin-hashicorp-aws/provisioner/docker-mailserver"
Expand All @@ -20,6 +21,7 @@ func main() {
pps.RegisterProvisioner("docker-mailserver-provisioner", new(dockerMailServerProv.Provisioner))
pps.RegisterProvisioner("kong-api-gateway-provisioner", new(kongApiGatewayProv.Provisioner))
pps.RegisterProvisioner("sonatype-nexus-repository-provisioner", new(sonatypeNexusRepository.Provisioner))
pps.RegisterProvisioner("webservice-provisioner", new(webservice.Provisioner))
pps.SetVersion(pluginVersion.PluginVersion)
err := pps.Run()
if err != nil {
Expand Down
89 changes: 82 additions & 7 deletions provisioner/basic-provisioner/basic-provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,98 @@
// SPDX-License-Identifier: MPL-2.0

// This package implements a provisioner for Packer that executes a specified list of shell commands within the remote
// machine
// machine. It doesn't use Packer's original shell provisioner
// (https://github.com/hashicorp/packer/blob/main/provisioner/shell/provisioner.go), which is essentially not fully
// exported for public use due to the unexported config parameter -
// https://github.com/hashicorp/packer/blob/1e446de977e93b7119ebbaa6f55268bd29240e4f/provisioner/shell/provisioner.go#L73
// This parameter is unexported because is not capitalized
//
// This provisioner works by loading all provided amiConfigCommands into a shell scripts. We do this because executing
// amiConfigCommands separately in the following way simply doesn't work:
//
// if len(amiConfigCommands) > 0 {
// for _, command := range amiConfigCommands {
// err := (&packersdk.RemoteCmd{Command: command}).RunWithUi(ctx, communicator, ui)
// if err != nil {
// return fmt.Errorf("CMD error: %s", err)
// }
// }
// }
//
// because each command is executed in separate shell, meaning their state is not preserved unless the state is written
// to the remote machines hard disk. For example, the regular env variable export like "export JAVA_HOME=..." won't
// carry over to the next command. The only way to preserve all in-memory states is to run everything in a one-time
// script, which is how this provisioner is implemented
package basicProvisioner

import (
"bufio"
"context"
"fmt"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/retry"
"github.com/hashicorp/packer-plugin-sdk/tmp"
"math/rand"
"os"
)

func Provision(ctx context.Context, ui packersdk.Ui, communicator packersdk.Communicator, amiConfigCommands []string) error {
if len(amiConfigCommands) > 0 {
for _, command := range amiConfigCommands {
err := (&packersdk.RemoteCmd{Command: command}).RunWithUi(ctx, communicator, ui)
if err != nil {
return err
}
scriptFile, err := tmp.File("packer-shell")
if err != nil {
return fmt.Errorf("Error while trying to load commands into a shell script: %s", err)
}
defer os.Remove(scriptFile.Name())

writer := bufio.NewWriter(scriptFile)
writer.WriteString("#!/bin/bash\n")
writer.WriteString("set -x\n")
writer.WriteString("set -e\n")
writer.WriteString("\n")
for _, command := range amiConfigCommands {
if _, err := writer.WriteString(command + "\n"); err != nil {
return fmt.Errorf("Error flushing command '%s' into a shell script: %s", command, err)
}
}
if err := writer.Flush(); err != nil {
return fmt.Errorf("Error flushing shell script: %s", err)
}

scriptFile.Close()

ui.Say(fmt.Sprintf("Provisioning with %s", amiConfigCommands))

f, err := os.Open(scriptFile.Name())
if err != nil {
return fmt.Errorf("Error opening shell script: %s", err)
}
defer f.Close()

var cmd *packersdk.RemoteCmd
err = retry.Config{}.Run(ctx, func(ctx context.Context) error {
if _, err := f.Seek(0, 0); err != nil {
return err
}

remotePath := fmt.Sprintf("%s/%s", "/tmp", fmt.Sprintf("script_%d.sh", rand.Intn(9999)))
if err := communicator.Upload(remotePath, f, nil); err != nil {
return fmt.Errorf("Error uploading script: %s", err)
}

cmd = &packersdk.RemoteCmd{
Command: fmt.Sprintf("chmod 0755 %s", remotePath),
}
if err := communicator.Start(ctx, cmd); err != nil {
return fmt.Errorf("Error chmodding script file to 0755 in remote machine: %s", err)
}
cmd.Wait()

cmd = &packersdk.RemoteCmd{Command: fmt.Sprintf("chmod +x %s; %s", remotePath, remotePath)}
return cmd.RunWithUi(ctx, communicator, ui)
})

if err != nil {
return err
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ func TestAccSonatypeNexusRepositoryProvisioner(t *testing.T) {
}
logsString := string(logsBytes)

t.Log(logsBytes)
errorString := "\\[ERROR\\] Remote command exited with"
if matched, _ := regexp.MatchString(".*"+errorString+".*", logsString); matched {
t.Fatalf("Acceptance tests for %s failed. Please search for '%s' in log file at %s", "sonatype-nexus-repository provisioner", errorString, logfile)
}

provisionerOutputLog := "amazon-ebs.hashicorp-aws: AMIs were created:"
if matched, _ := regexp.MatchString(provisionerOutputLog+".*", logsString); !matched {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ packer {
}

source "amazon-ebs" "hashicorp-aws" {
ami_name = "packer-plugin-hashicorp-aws-acc-test-ami"
ami_name = "packer-plugin-hashicorp-aws-acc-test-ami-sonatype-nexus-repository"
force_deregister = "true"
force_delete_snapshot = "true"

Expand Down
4 changes: 2 additions & 2 deletions provisioner/ssl-provisioner/ssl-provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ func Provision(ctx context.Context, interCtx interpolate.Context, ui packersdk.U
sslCertKey, err := decodeBase64(sslCertKeyBase64)
sslCertKeySource, err := WriteToFile(sslCertKey)
sslCertKeyDestination := fmt.Sprintf(filepath.Join(homeDir, "ssl.key"))
err = fileProvisioner.Provision(interCtx, ui, communicator, sslCertSource, sslCertDestination)
err = fileProvisioner.Provision(interCtx, ui, communicator, sslCertKeySource, sslCertKeyDestination)
if err != nil {
return fmt.Errorf("error uploading '%s' to '%s': %s", sslCertKeySource, sslCertKeyDestination, err)
}

if nginxConfig != "" {
nginxSource, err := WriteToFile(nginxConfig)
nginxDst := fmt.Sprintf(filepath.Join(homeDir, "nginx-ssl.conf"))
err = fileProvisioner.Provision(interCtx, ui, communicator, sslCertSource, sslCertDestination)
err = fileProvisioner.Provision(interCtx, ui, communicator, nginxSource, nginxDst)
if err != nil {
return fmt.Errorf("error uploading '%s' to '%s': %s", nginxSource, nginxDst, err)
}
Expand Down
95 changes: 95 additions & 0 deletions provisioner/webservice/provisioner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Jiaqi Liu
// SPDX-License-Identifier: MPL-2.0

//go:generate packer-sdc mapstructure-to-hcl2 -type Config

package webservice

import (
"context"
"fmt"
basicProvisioner "github.com/QubitPi/packer-plugin-hashicorp-aws/provisioner/basic-provisioner"
fileProvisioner "github.com/QubitPi/packer-plugin-hashicorp-aws/provisioner/file-provisioner"
sslProvisioner "github.com/QubitPi/packer-plugin-hashicorp-aws/provisioner/ssl-provisioner"
"github.com/hashicorp/hcl/v2/hcldec"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
"path/filepath"
)

type Config struct {
WarSource string `mapstructure:"warSource" required:"true"`
HomeDir string `mapstructure:"homeDir" required:"false"`

ctx interpolate.Context
}

type Provisioner struct {
config Config
}

func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec {
return p.config.FlatMapstructure().HCL2Spec()
}

func (p *Provisioner) Prepare(raws ...interface{}) error {
err := config.Decode(&p.config, nil, raws...)
if err != nil {
return err
}

return nil
}

func (p *Provisioner) Provision(ctx context.Context, ui packersdk.Ui, communicator packersdk.Communicator, generatedData map[string]interface{}) error {
p.config.HomeDir = sslProvisioner.GetHomeDir(p.config.HomeDir)

warFileDst := fmt.Sprintf(filepath.Join(p.config.HomeDir, "ROOT.war"))

err := fileProvisioner.Provision(p.config.ctx, ui, communicator, p.config.WarSource, warFileDst)
if err != nil {
return err
}

return basicProvisioner.Provision(ctx, ui, communicator, getCommands(p.config.HomeDir))
}

func getCommands(homeDir string) []string {
return append(
getCommandsUpdatingUbuntu(),
append(getCommandsInstallingJDK17(), getCommandsInstallingJetty(homeDir)...)...,
)
}

func getCommandsUpdatingUbuntu() []string {
return []string{
"sudo apt update && sudo apt upgrade -y",
"sudo apt install software-properties-common -y",
}
}

// Install JDK 17 - https://www.rosehosting.com/blog/how-to-install-java-17-lts-on-ubuntu-20-04/
func getCommandsInstallingJDK17() []string {
return []string{
"sudo apt update -y",
"sudo apt install openjdk-17-jdk -y",
"export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64",
}
}

// Install and configure Jetty (for JDK 17) container
func getCommandsInstallingJetty(homeDir string) []string {
return []string{
"export JETTY_VERSION=11.0.15",
"wget https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/$JETTY_VERSION/jetty-home-$JETTY_VERSION.tar.gz",
"tar -xzvf jetty-home-$JETTY_VERSION.tar.gz",
"rm jetty-home-$JETTY_VERSION.tar.gz",
fmt.Sprintf("export JETTY_HOME=%s/jetty-home-$JETTY_VERSION", homeDir),
"mkdir jetty-base",
"cd jetty-base",
"java -jar $JETTY_HOME/start.jar --add-module=annotations,server,http,deploy,servlet,webapp,resources,jsp",
fmt.Sprintf("mv %s/ROOT.war webapps/ROOT.war", homeDir),
"cd ../",
}
}
33 changes: 33 additions & 0 deletions provisioner/webservice/provisioner.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ad2a0a4

Please sign in to comment.