© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
A. FreemanPro Gohttps://doi.org/10.1007/978-1-4842-7355-5_38

38. SportsStore: Finishing and Deployment

Adam Freeman1  
(1)
London, UK
 

In this chapter, I complete the development of the SportsStore application and prepare it for deployment.

Tip

You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/apress/pro-go. See Chapter 2 for how to get help if you have problems running the examples.

Completing the Administration Features

Two of the four administration sections defined in Chapter 37 are not yet implemented. I define both of these features at the same time in this section, reflecting the fact that they are simpler than the product and category features.

Extending the Repository

Two new repository methods are required to complete the administration features, as shown in Listing 38-1.
package models
type Repository interface {
    GetProduct(id int) Product
    GetProducts() []Product
    SaveProduct(*Product)
    GetProductPage(page, pageSize int) (products []Product, totalAvailable int)
    GetProductPageCategory(categoryId int, page, pageSize int) (products []Product,
        totalAvailable int)
    GetCategories() []Category
    SaveCategory(*Category)
    GetOrder(id int) []Order
    GetOrders() Order
    SaveOrder(*Order)
    SetOrderShipped(*Order)
    Seed()
    Init()
}
Listing 38-1

Adding an Interface in the repository.go File in the models Folder

The SetOrderShipped method will be used to update an existing Order to indicate when it has been shipped. The Init method corresponds to a method name already defined by the SQL implementation of the interface and will be used to allow the administrator to prepare the database for first use after it has been deployed.

To define the SQL that will be used to update existing orders, add a file named update_order.sql to the sportsstore/sql folder with the content shown in Listing 38-2.
UPDATE Orders SET Shipped = ? WHERE Id == ?
Listing 38-2

The Contents of the update_order.sql File in the sql Folder

Listing 38-3 adds a new command so that the SQL defined in Listing 38-2 can be accessed in the same way as the other SQL statements.
...
type SqlCommands struct {
    Init,
    Seed,
    GetProduct,
    GetProducts,
    GetCategories,
    GetPage,
    GetPageCount,
    GetCategoryPage,
    GetCategoryPageCount,
    GetOrder,
    GetOrderLines,
    GetOrders,
    GetOrdersLines,
    SaveOrder,
    SaveOrderLine,
    UpdateOrder,
    SaveProduct,
    UpdateProduct,
    SaveCategory,
    UpdateCategory *sql.Stmt
}
...
Listing 38-3

Adding a New Command in the sql_repo.go File in the models/repo Folder

Listing 38-4 adds a configuration setting that specifies the location of the SQL required for the new command.
...
"sql": {
    "connection_str": "store.db",
    "always_reset": true,
    "commands": {
        "Init": "sql/init_db.sql",
        "Seed": "sql/seed_db.sql",
        "GetProduct": "sql/get_product.sql",
        "GetProducts": "sql/get_products.sql",
        "GetCategories": "sql/get_categories.sql",
        "GetPage": "sql/get_product_page.sql",
        "GetPageCount": "sql/get_page_count.sql",
        "GetCategoryPage": "sql/get_category_product_page.sql",
        "GetCategoryPageCount": "sql/get_category_product_page_count.sql",
        "GetOrder": "sql/get_order.sql",
        "GetOrderLines": "sql/get_order_lines.sql",
        "GetOrders": "sql/get_orders.sql",
        "GetOrdersLines": "sql/get_orders_lines.sql",
        "SaveOrder": "sql/save_order.sql",
        "SaveOrderLine": "sql/save_order_line.sql",
        "SaveProduct":          "sql/save_product.sql",
        "UpdateProduct":        "sql/update_product.sql",
        "SaveCategory":         "sql/save_category.sql",
        "UpdateCategory":       "sql/update_category.sql",
        "UpdateOrder":          "sql/update_order.sql"
    }
}
...
Listing 38-4

Adding a Configuration Setting in the config.json File in the sportsstore Folder

To implement the repository method, add a file named sql_order_update.go in the sportsstore/models/repo folder with the content shown in Listing 38-5.
package repo
import "sportsstore/models"
func (repo *SqlRepository) SetOrderShipped(o *models.Order) {
    result, err := repo.Commands.UpdateOrder.ExecContext(repo.Context,
        o.Shipped, o.ID)
    if err == nil {
        rows, err :=result.RowsAffected()
        if err != nil {
            repo.Logger.Panicf("Cannot get updated ID: %v", err.Error())
        } else if rows != 1 {
            repo.Logger.Panicf("Got unexpected rows affected: %v", rows)
        }
    } else {
        repo.Logger.Panicf("Cannot exec UpdateOrder command: %v", err.Error())
    }
}
Listing 38-5

The Contents of the sql_order_update.go File in the models/repo Folder

Implementing the Request Handlers

To add support for managing orders, replace the content of the orders_handler.go file in the sportsstore/admin folder with the content shown in Listing 38-6.
package admin
import (
    "platform/http/actionresults"
    "platform/http/handling"
    "sportsstore/models"
)
type OrdersHandler struct {
    models.Repository
    handling.URLGenerator
}
func (handler OrdersHandler) GetData() actionresults.ActionResult {
    return actionresults.NewTemplateAction("admin_orders.html", struct {
        Orders []models.Order
         CallbackUrl string
    }{
        Orders: handler.Repository.GetOrders(),
        CallbackUrl: mustGenerateUrl(handler.URLGenerator,
            OrdersHandler.PostOrderToggle),
    })
}
func (handler OrdersHandler) PostOrderToggle(ref EditReference) actionresults.ActionResult {
    order := handler.Repository.GetOrder(ref.ID)
    order.Shipped = !order.Shipped
    handler.Repository.SetOrderShipped(&order)
    return actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
        AdminHandler.GetSection, "Orders"))
}
Listing 38-6

The New Contents of the orders_handler.go File in the admin Folder

The only change that will be allowed on orders is to change the value of the Shipped field, indicating that the order has been dispatched. Replace the content of the database_handler.go file with the content shown in Listing 38-7.
package admin
import (
    "platform/http/actionresults"
    "platform/http/handling"
    "sportsstore/models"
)
type DatabaseHandler struct {
    models.Repository
    handling.URLGenerator
}
func (handler DatabaseHandler) GetData() actionresults.ActionResult {
    return actionresults.NewTemplateAction("admin_database.html", struct {
        InitUrl, SeedUrl string
    }{
        InitUrl: mustGenerateUrl(handler.URLGenerator,
            DatabaseHandler.PostDatabaseInit),
        SeedUrl: mustGenerateUrl(handler.URLGenerator,
           DatabaseHandler.PostDatabaseSeed),
    })
}
func (handler DatabaseHandler) PostDatabaseInit() actionresults.ActionResult {
    handler.Repository.Init()
    return actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
        AdminHandler.GetSection, "Database"))
}
func (handler DatabaseHandler) PostDatabaseSeed() actionresults.ActionResult {
    handler.Repository.Seed()
    return actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
        AdminHandler.GetSection, "Database"))
}
Listing 38-7

The New Contents of the database_handler.go File in the admin Folder

There are handler methods for each of the operations that can be performed on the database, which will allow the administrator to jump-start the application after the application has been prepared for deployment later in this chapter.

Creating the Templates

To create the template used to manage orders, add a file named admin_orders.html to the sportsstore/templates folder with the content shown in Listing 38-8.
{{ $context := .}}
<table class="table table-sm table-striped table-bordered">
    <tr><th>ID</th><th>Name</th><th>Address</th><th/></tr>
    <tbody>
        {{ range $context.Orders }}
            <tr>
                <td>{{ .ID }}</td>
                <td>{{ .Name }}</td>
                <td>{{ .StreetAddr }}, {{ .City }}, {{ .State }},
                     {{ .Country }}, {{ .Zip }}</td>
                <td>
                    <form method="POST" action="{{$context.CallbackUrl}}">
                        <input type="hidden" name="id" value="{{.ID}}" />
                        {{ if .Shipped }}
                            <button class="btn-btn-sm btn-warning" type="submit">
                                Ship Order
                            </button>
                        {{ else }}
                            <button class="btn-btn-sm btn-danger" type="submit">
                                Mark Unshipped
                            </button>
                        {{ end }}
                    </form>
                </td>
            </tr>
            <tr><th colspan="2"/><th>Quantity</th><th>Product</th></tr>
            {{ range .Products }}
                <tr>
                    <td colspan="2"/>
                    <td>{{ .Quantity }}</td>
                    <td>{{ .Product.Name }}</td>
                </tr>
            {{ end }}
        {{ end }}
    </tbody>
</table>
Listing 38-8

The Contents of the admin_orders.html File in the templates Folder

The template displays the orders in a table, along with details of the products that each contains. To create the template used to manage the database, add a file named admin_database.html to the sportsstore/templates folder with the content shown in Listing 38-9.
{{ $context := . }}
<form method="POST">
    <button class="btn btn-danger m-3 p-2" type="submit"
            formaction="{{ $context.InitUrl}}">
        Initialize Database
    </button>
    <button class="btn btn-warning m-3 p-2" type="submit"
            formaction="{{ $context.SeedUrl}}">
        Seed Database
    </button>
</form>
Listing 38-9

The Contents of the admin_database.html File in the templates Folder

Compile and execute the project, use a browser to request http://localhost:5000/admin, and click the Orders button to see the orders in the database and change their shipping status, as shown in Figure 38-1. Click the Database button, and you will be able to reset and seed the database, also shown in Figure 38-1.
Figure 38-1

Completing the administration features

Restricting Access to the Administration Features

Granting open access to the administration features simplifies development but should never be allowed in production. Now that the administration features are complete, it is time to make sure they are available only to authorized users.

Creating the User Store and Request Handler

As I explained previously, I do not implement a real authentication system, which is difficult to do securely and is beyond the scope of this book. Instead, I am going to follow a similar approach to the one I took with the platform project and rely on hardwired credentials to authenticate a user. Create the sportsstore/admin/auth folder and add to it a file named user_store.go with the content shown in Listing 38-10.
package auth
import (
    "platform/services"
    "platform/authorization/identity"
    "strings"
)
func RegisterUserStoreService() {
    err := services.AddSingleton(func () identity.UserStore {
        return &userStore{}
    })
    if (err != nil) {
        panic(err)
    }
}
var users = map[int]identity.User {
    1: identity.NewBasicUser(1, "Alice", "Administrator"),
}
type userStore struct {}
func (store *userStore) GetUserByID(id int) (identity.User, bool) {
    user, found := users[id]
    return user, found
}
func (store *userStore) GetUserByName(name string) (identity.User, bool) {
    for _, user := range users {
        if strings.EqualFold(user.GetDisplayName(), name) {
            return user, true
        }
    }
    return nil, false
}
Listing 38-10

The Contents of the user_store.go File in the admin/auth Folder

To create the handler for authentication requests, add a file named auth_handler.go to the sportsstore/admin folder with the content shown in Listing 38-11.
package admin
import (
    "platform/authorization/identity"
    "platform/http/actionresults"
    "platform/http/handling"
    "platform/sessions"
)
type AuthenticationHandler struct {
    identity.User
    identity.SignInManager
    identity.UserStore
    sessions.Session
    handling.URLGenerator
}
const SIGNIN_MSG_KEY string = "signin_message"
func (handler AuthenticationHandler) GetSignIn() actionresults.ActionResult {
    message := handler.Session.GetValueDefault(SIGNIN_MSG_KEY, "").(string)
    return actionresults.NewTemplateAction("signin.html", message)
}
type Credentials struct {
    Username string
    Password string
}
func (handler AuthenticationHandler) PostSignIn(creds Credentials) actionresults.ActionResult {
    if creds.Password == "mysecret" {
        user, ok := handler.UserStore.GetUserByName(creds.Username)
        if (ok) {
            handler.Session.SetValue(SIGNIN_MSG_KEY, "")
            handler.SignInManager.SignIn(user)
            return actionresults.NewRedirectAction("/admin/section/")
        }
    }
    handler.Session.SetValue(SIGNIN_MSG_KEY, "Access Denied")
    return actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
        AuthenticationHandler.GetSignIn))
}
func (handler AuthenticationHandler) PostSignOut(creds Credentials) actionresults.ActionResult {
        handler.SignInManager.SignOut(handler.User)
    return actionresults.NewRedirectAction("/")
}
Listing 38-11

The Contents of the auth_handler.go File in the admin Folder

The GetSignIn method renders a template that will prompt the user for their credentials and displays a message that is stored in the session. The PostSignIn method receives the credentials from the form and either signs the user into the application or adds a message to the session and redirects the browser so the user can try again.

To create the template to let users sign into the application, add a file named signin.html to the sportsstore/templates folder with the content shown in Listing 38-12.
{{ layout "simple_layout.html" }}
{{ if ne . "" }}
    <h3 class="text-danger p-2">{{ . }}</h3>
{{ end }}
<form method="POST" class="m-2">
    <div class="form-group">
        <label>Username:</label>
        <input class="form-control"  name="username" />
    </div>
    <div class="form-group">
        <label>Password:</label>
        <input class="form-control" name="password" type="password" />
    </div>
    <div class="my-2">
        <button class="btn btn-secondary" type="submit">Sign In</button>
    </div>
</form>
Listing 38-12

The Contents of the signin.html File in the templates Folder

This template prompts the user for their account name and password, which is posted back to the request handler.

To allow the user to sign out of the application, add a file named signout_handler.go to the sportsstore/admin folder with the content shown in Listing 38-13.
package admin
import (
          "platform/authorization/identity"
          "platform/http/actionresults"
    "platform/http/handling"
)
type SignOutHandler struct {
    identity.User
    handling.URLGenerator
}
func (handler SignOutHandler) GetUserWidget() actionresults.ActionResult {
        return actionresults.NewTemplateAction("user_widget.html", struct {
            identity.User
            SignoutUrl string}{
                handler.User,
                mustGenerateUrl(handler.URLGenerator,
                    AuthenticationHandler.PostSignOut),
            })
    }
Listing 38-13

The Contents of the signout_handler.go File in the admin Folder

To create the template that will let the user sign out, add a file named user_widget.html to the sportsstore/templates folder with the content shown in Listing 38-14.
{{ $context := . }}
{{ if $context.User.IsAuthenticated }}
    <form method="POST" action="{{$context.SignoutUrl}}">
        <button class="btn btn-sm btn-outline-secondary text-white" type="submit">
            Sign Out
        </button>
    </form>
{{ end }}
Listing 38-14

The Contents of the user_widget.html File in the templates Folder

Listing 38-15 adds the user widget to the layout used for the administration features.
...
<div class="bg-info text-white p-2">
    <div class="container-fluid">
        <div class="row">
            <div class="col navbar-brand">SPORTS STORE Administration</div>
            <div class="col-6 navbar-text text-end">
                {{ handler "signout" "getuserwidget" }}
            </div>
        </div>
    </div>
</div>
...
Listing 38-15

Adding a Widget in the admin.html File in the templates Folder

Configuring the Application

Listing 38-16 adds a configuration setting that specifies a URL that will be used when a request is made for a restricted URL, which provides a more useful alternative to returning a status code.
{
    "logging" : {
        "level": "debug"
    },
    "files": {
        "path": "files"
    },
    "templates": {
        "path": "templates/*.html",
        "reload": true
    },
    "sessions": {
        "key": "MY_SESSION_KEY",
        "cyclekey": true
    },
    "sql": {
         // ...setting omitted for brevity...
    },
    "authorization": {
        "failUrl": "/signin"
    }
}
Listing 38-16

Adding a Configuration Setting in the config.json File in the sportsstore Folder

The specified URL will prompt the user for their credentials. Listing 38-17 reconfigures the request pipeline so the administration features are protected.
package main
import (
    "sync"
    "platform/http"
    "platform/http/handling"
    "platform/services"
    "platform/pipeline"
    "platform/pipeline/basic"
    "sportsstore/store"
    "sportsstore/models/repo"
    "platform/sessions"
    "sportsstore/store/cart"
    "sportsstore/admin"
    "platform/authorization"
    "sportsstore/admin/auth"
)
func registerServices() {
    services.RegisterDefaultServices()
    //repo.RegisterMemoryRepoService()
    repo.RegisterSqlRepositoryService()
    sessions.RegisterSessionService()
    cart.RegisterCartService()
    authorization.RegisterDefaultSignInService()
    authorization.RegisterDefaultUserService()
    auth.RegisterUserStoreService()
}
func createPipeline() pipeline.RequestPipeline {
    return pipeline.CreatePipeline(
        &basic.ServicesComponent{},
        &basic.LoggingComponent{},
        &basic.ErrorComponent{},
        &basic.StaticFileComponent{},
        &sessions.SessionComponent{},
        authorization.NewAuthComponent(
            "admin",
            authorization.NewRoleCondition("Administrator"),
            admin.AdminHandler{},
            admin.ProductsHandler{},
            admin.CategoriesHandler{},
            admin.OrdersHandler{},
            admin.DatabaseHandler{},
            admin.SignOutHandler{},
        ).AddFallback("/admin/section/", "^/admin[/]?$"),
        handling.NewRouter(
            handling.HandlerEntry{ "",  store.ProductHandler{}},
            handling.HandlerEntry{ "",  store.CategoryHandler{}},
            handling.HandlerEntry{ "", store.CartHandler{}},
            handling.HandlerEntry{ "", store.OrderHandler{}},
            // handling.HandlerEntry{ "admin", admin.AdminHandler{}},
            // handling.HandlerEntry{ "admin", admin.ProductsHandler{}},
            // handling.HandlerEntry{ "admin", admin.CategoriesHandler{}},
            // handling.HandlerEntry{ "admin", admin.OrdersHandler{}},
            // handling.HandlerEntry{ "admin", admin.DatabaseHandler{}},
            handling.HandlerEntry{ "", admin.AuthenticationHandler{}},
        ).AddMethodAlias("/", store.ProductHandler.GetProducts, 0, 1).
            AddMethodAlias("/products[/]?[A-z0-9]*?",
                store.ProductHandler.GetProducts, 0, 1),    )
}
func main() {
    registerServices()
    results, err := services.Call(http.Serve, createPipeline())
    if (err == nil) {
        (results[0].(*sync.WaitGroup)).Wait()
    } else {
        panic(err)
    }
}
Listing 38-17

Configuring the Application in the main.go File in the sportsstore Folder

Compile and execute the application and use a browser to request http://localhost:5000/admin. When prompted, authenticate as user alice with password mysecret, and you will be granted access to the administration features, as shown in Figure 38-2.
Figure 38-2

Signing into the application

Creating a Web Service

The final feature I am going to add is a simple web service, just to show how it can be done. I am not going to use authorization to protect the web service, which can be a complex process that depends on the type of clients expected to require access. This means that any user will be able to modify the database. If you are deploying a real web service, then you can use cookies in much the same way as I have done in this example. If your clients don’t support cookies, then JSON Web Tokens (JWTs) can be used, as described at https://jwt.io.

To create the web service, add a file named rest_handler.go to the sportsstore/store folder with the content shown in Listing 38-18.
package store
import (
    "sportsstore/models"
    "platform/http/actionresults"
    "net/http"
)
type StatusCodeResult struct {
    code int
}
func (action *StatusCodeResult) Execute(ctx *actionresults.ActionContext) error {
    ctx.ResponseWriter.WriteHeader(action.code)
    return nil
}
type RestHandler struct {
    Repository models.Repository
}
func (h RestHandler) GetProduct(id int) actionresults.ActionResult {
    return actionresults.NewJsonAction(h.Repository.GetProduct(id))
}
func (h RestHandler) GetProducts() actionresults.ActionResult {
    return actionresults.NewJsonAction(h.Repository.GetProducts())
}
type ProductReference struct {
    models.Product
    CategoryID int
}
func (h RestHandler) PostProduct(p ProductReference) actionresults.ActionResult {
    if p.ID == 0 {
        return actionresults.NewJsonAction(h.processData(p))
    } else {
        return &StatusCodeResult{ http.StatusBadRequest }
    }
}
func (h RestHandler) PutProduct(p ProductReference) actionresults.ActionResult {
    if p.ID > 0 {
        return actionresults.NewJsonAction(h.processData(p))
    } else {
        return &StatusCodeResult{ http.StatusBadRequest }
    }
}
func (h RestHandler) processData(p ProductReference) models.Product {
    product := p.Product
    product.Category = &models.Category {
        ID: p.CategoryID,
    }
    h.Repository.SaveProduct(&product)
    return h.Repository.GetProduct(product.ID)
}
Listing 38-18

The Contents of the rest_handler.go File in the store Folder

The StatusCodeResult struct is an action result that sends an HTTP status code, which is useful for web services. The request handler defines methods that allow one product and all products to be retrieved using GET requests, new products to be created using POST requests, and existing products to be modified using PUT requests. Listing 38-19 registers the new handler with a prefix of /api.
...
handling.NewRouter(
     handling.HandlerEntry{ "",  store.ProductHandler{}},
     handling.HandlerEntry{ "",  store.CategoryHandler{}},
     handling.HandlerEntry{ "", store.CartHandler{}},
     handling.HandlerEntry{ "", store.OrderHandler{}},
     handling.HandlerEntry{ "", admin.AuthenticationHandler{}},
     handling.HandlerEntry{ "api", store.RestHandler{}},
).AddMethodAlias("/", store.ProductHandler.GetProducts, 0, 1).
    AddMethodAlias("/products[/]?[A-z0-9]*?",
    store.ProductHandler.GetProducts, 0, 1),
...
Listing 38-19

Registering a Handler in the main.go File in the sportsstore Folder

Compile and execute the project. Open a new command prompt and execute the command shown in Listing 38-20 to add a new product to the database.
curl --header "Content-Type: application/json" --request POST --data '{"name" : "Jet Engine","description": "Paddling is hard work", "price":650, "categoryid":1}' http://localhost:5000/api/product
Listing 38-20

Adding a New Product

If you are using Windows, open a new PowerShell window and run the command shown in Listing 38-21.
Invoke-RestMethod http://localhost:5000/api/product -Method POST -Body  (@{ Name="Jet Engine"; Description="Paddling is hard work"; Price=650; CategoryId=1 } | ConvertTo-Json) -ContentType "application/json"
Listing 38-21

Adding a New Product in Windows

To see the effect of the change, run the command shown in Listing 38-22.
curl http://localhost:5000/api/product/10
Listing 38-22

Requesting Data

If you are using Windows, run the command shown in Listing 38-23 in a PowerShell window.
Invoke-RestMethod http://localhost:5000/api/product/10
Listing 38-23

Requesting Data in Windows

You can also use a browser to see the effect of the change. Request http://localhost:5000/admin. Authenticate as the user alice with the password mysecret and click the Products button. The last table row will contain the product created using the web service, as shown in Figure 38-3.
Figure 38-3

Checking the effect of a database change

Preparing for Deployment

In this section, I will prepare the SportsStore application and create a container that can be deployed into production. This isn’t the only way that a Go application can be deployed, but I picked Docker containers because they are widely used and because they suit web applications. This is not a complete guide to deployment, but it will give you a sense of the process to prepare an application.

Installing the Certificates

The first step is to add the certificates that will be used for HTTPS. As explained in Chapter 24, you can create a self-signed certificate if you don’t have a real certificate available, or you can use the certificate files from the GitHub repository for this book (which contain a self-signed certificate that I created).

Configuring the Application

The most important change is to change the application configuration to disable features that are convenient during development but that should not be used in deployment, as well as to enable HTTPS, as shown in Listing 38-24.
{
    "logging" : {
        "level": "information"
    },
    "files": {
        "path": "files"
    },
    "templates": {
        "path": "templates/*.html",
        "reload": false
    },
    "sessions": {
        "key": "MY_SESSION_KEY",
        "cyclekey": false
    },
    "sql": {
        "connection_str": "store.db",
        "always_reset": false,
        "commands": {
            "Init": "sql/init_db.sql",
            "Seed": "sql/seed_db.sql",
            "GetProduct": "sql/get_product.sql",
            "GetProducts": "sql/get_products.sql",
            "GetCategories": "sql/get_categories.sql",
            "GetPage": "sql/get_product_page.sql",
            "GetPageCount": "sql/get_page_count.sql",
            "GetCategoryPage": "sql/get_category_product_page.sql",
            "GetCategoryPageCount": "sql/get_category_product_page_count.sql",
            "GetOrder": "sql/get_order.sql",
            "GetOrderLines": "sql/get_order_lines.sql",
            "GetOrders": "sql/get_orders.sql",
            "GetOrdersLines": "sql/get_orders_lines.sql",
            "SaveOrder": "sql/save_order.sql",
            "SaveOrderLine": "sql/save_order_line.sql",
            "SaveProduct":          "sql/save_product.sql",
            "UpdateProduct":        "sql/update_product.sql",
            "SaveCategory":         "sql/save_category.sql",
            "UpdateCategory":       "sql/update_category.sql",
            "UpdateOrder":          "sql/update_order.sql"
        }
    },
    "authorization": {
        "failUrl": "/signin"
    },
    "http": {
        "enableHttp": false,
        "enableHttps": true,
        "httpsPort": 5500,
        "httpsCert": "certificate.cer",
        "httpsKey": "certificate.key"
    }
}
Listing 38-24

Changing Settings in the config.json File in the sportsstore Folder

Make sure that the values you specify for the httpsCert and httpsKey properties match the names of your certificate files and that the certificate files are in the sportsstore folder.

Building the Application

Docker containers run Linux. If you are running Windows, you must select Linux as the build target by running the commands shown in Listing 38-25 in a PowerShell window to configure the Go build tools. This isn’t required if you are running Linux.
$Env:GOOS = "linux"; $Env:GOARCH = "amd64"
Listing 38-25

Setting Linux as the Build Target

Run the command shown in Listing 38-26 in the sportsstore folder to build the application.
go build
Listing 38-26

Building the Application

Note

If you are a Windows user, you can return to a normal Windows build with the following command: $Env:GOOS = "windows"; $Env:GOARCH = "amd64". But don’t run this command until you have completed the deployment process.

Installing Docker Desktop

Go to docker.com and download and install the Docker Desktop package. Follow the installation process, reboot your machine, and run the command shown in Listing 38-27 to check that Docker has been installed and is in your path. (The Docker installation process seems to change often, which is why I am not being more specific about the process.)

Note

You will have to create an account on docker.com to download the installer.

docker --version
Listing 38-27

Checking the Docker Desktop Installation

Creating the Docker Configuration Files

To create the Docker configuration for the application, create a file named Dockerfile in the sportsstore folder with the content shown in Listing 38-28.
FROM alpine:latest
COPY sportsstore /app/
COPY templates /app/templates
COPY sql/* /app/sql/
COPY files/* /app/files/
COPY config.json /app/
COPY certificate.* /app/
EXPOSE 5500
WORKDIR /app
ENTRYPOINT ["./sportsstore"]
Listing 38-28

The Contents of the Dockerfile in the sportsstore Folder

These instructions copy the application and its supporting files into a Docker image and configure its execution. The next step is to create an image using the instructions defined in Listing 38-28. Run the command shown in Listing 38-29 in the sportsstore folder to create a Docker image.
docker build  --tag go_sportsstore .
Listing 38-29

Creating an Image

Ensure that you have stopped all other instances of the application and run the command shown in Listing 38-30 to create a new container from the image and execute it.
docker run -p 5500:5500 go_sportsstore
Listing 38-30

Creating and Starting a Container

Give the container a moment to start and then use a browser to request https://localhost:5500, which will produce the response shown in Figure 38-4. If you have used a self-signed certificate, then you may have to pass through a security warning.
Figure 38-4

Running the application in a container

The application is now ready for deployment. To stop the container—and any other container that is running—run the command shown in Listing 38-31.
docker kill $(docker ps -q)
Listing 38-31

Stopping Containers

Summary

In this chapter, I completed the SportsStore application by finishing the administration features, configuring authorization, and creating a basic web service, before preparing the application for deployment using a Docker container.

That’s all I have to teach you about Go. I can only hope that you have enjoyed reading this book as much as I enjoyed writing it, and I wish you every success in your Go projects.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.27.58