I've created a simple API following a youtube tutorial that works perfectly locally. Once I containerise the app and run the container, I can't access the API at http://localhost:8080. I'm guessing it has something to do with the port settings I'm using in the dockerfile, but I'm not sure.
main.go file:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"errors"
)
type phone struct{
ID string `json:"id"`
Model string `json:"model"`
Year string `json:"year"`
Quantity int `json:"quantity"`
}
var phones = []phone{
{ID: "1", Model: "iPhone 11", Year: "2019", Quantity: 4},
{ID: "2", Model: "iPhone 6", Year: "2014", Quantity: 9},
{ID: "3", Model: "iPhone X", Year: "2017", Quantity: 2},
}
func phoneById(c *gin.Context) {
id := c.Param("id")
phone, err := getPhoneById(id)
if err != nil {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "Phone not found."})
return
}
c.IndentedJSON(http.StatusOK, phone)
}
func checkoutPhone(c *gin.Context) {
id, ok := c.GetQuery("id")
if !ok {
c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Missing id query paramater"})
return
}
phone, err := getPhoneById(id)
if err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Phone not found"})
return
}
if phone.Quantity <= 0 {
c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Phone not available."})
return
}
phone.Quantity -= 1
c.IndentedJSON(http.StatusOK, phone)
}
func returnPhone(c *gin.Context) {
id, ok := c.GetQuery("id")
if !ok {
c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Missing id query paramater"})
return
}
phone, err := getPhoneById(id)
if err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Phone not found"})
return
}
if phone.Quantity <= 0 {
c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Phone not available."})
return
}
phone.Quantity += 1
c.IndentedJSON(http.StatusOK, phone)
}
func getPhoneById(id string) (*phone, error) {
for i, p := range phones {
if p.ID == id {
return &phones[i], nil
}
}
return nil, errors.New("Phone not found.")
}
func getPhones(c *gin.Context) {
c.IndentedJSON(http.StatusOK, phones)
}
func createPhone(c *gin.Context) {
var newPhone phone
if err := c.BindJSON(&newPhone); err != nil {
return
}
phones = append(phones, newPhone)
c.IndentedJSON(http.StatusCreated, newPhone)
}
func main(){
router := gin.Default()
router.GET("/phones", getPhones)
router.GET("/phones/:id", phoneById)
router.POST("/phones", createPhone)
router.PATCH("/checkout", checkoutPhone)
router.PATCH("/return", returnPhone)
router.Run("localhost:8080")
}
and my dockerfile:
#The standard golang image contains all of the resources to build
#But is very large. So build on it, then copy the output to the
#final runtime container
FROM golang:latest AS buildContainer
WORKDIR /go/src/app
COPY . .
#flags: -s -w to remove symbol table and debug info
#CGO_ENALBED=0 is required for the code to run properly when copied alpine
RUN CGO_ENABLED=0 GOOS=linux go build -v -mod mod -ldflags "-s -w" -o restapi .
#Now build the runtime container, just a stripped down linux and copy the
#binary to it.
FROM alpine:latest
WORKDIR /app
COPY --from=buildContainer /go/src/app/restapi .
ENV GIN_MODE release
ENV HOST 0.0.0.0
ENV PORT 8080
EXPOSE 8080
CMD ["./restapi"]
I've tried different dockerfiles found on Google, and tried creating my own from scratch.
You need to bind to the public network interface inside the container. Because each container is its own host, and when you bind to the loopback interface inside, it will not be accessible to the outside world.
router.Run("0.0.0.0:8080")
Additionally, make sure you publish this port when running the container.
docker run --publish 8080:8080 myapp
You actually indicate the right intend with your environment variables, but they are not used in your code.
ENV HOST 0.0.0.0
ENV PORT 8080
You can use os.Getenv or os.LookupEnv to get those variables from your code and use them.
Related
Motivation
I'm running this command inside the container:
docker run -it --rm \
--mount type=volume,src=synapse-data,dst=/data \
-e SYNAPSE_SERVER_NAME=my.matrix.host \
-e SYNAPSE_REPORT_STATS=yes \
matrixdotorg/synapse:latest generate
Based on https://github.com/matrix-org/synapse/tree/v1.56.0/docker
Docker SDK usage
And I'm using this abstraction: https://pkg.go.dev/github.com/docker/docker/client#Client.ContainerCreate
As a general concept I want to use:
AutoRemove: true,
The point is to automate/enforce containers deletion after use, for instance, if the setup exits unexpectedly. I'm also using a container name: server_setup_temporary_container which hints the user that this is used during setup and is meant to be temporary. In case the setup did not shutdown the container, the user can do this and the bound volumes are freed.
My problem with this generate script
I can't use https://github.com/moby/moby/blob/v20.10.18/client/container_logs.go#L36 as the container exits once it finished executing the generate. Therefore I can't access the logs at all as they are already deleted.
In contrast, this works well with the postgresql container, as it runs as a daemon and needs explicit shutdown. The same concept fails with only executing a script!
I don't know how to continue here.
A few thoughts I had:
after generate execute a 'sleep 3600' and then explicitly shut the container down as well
try to get the logs from ContainerStart or ContainerCreate directly but studying the API this is probably not implemented this way
What I would not want is to remove the AutoRemove: true concept.
The source code
Using my StartContainer abstraction
// Start and run container
containerId, err := s.dockerClient.myStartContainer(docker.ContainerStartConfig{
Image: matrixImage,
Volumes: []docker.ContainerVolume{
docker.ContainerVolume{
Source: volume,
Target: "/data",
},
},
Env: []string{
fmt.Sprintf("SYNAPSE_SERVER_NAME=%s", domain),
"SYNAPSE_REPORT_STATS=no",
},
Cmds: []string{
"generate",
},
})
StartContainer abstraction
func (c *Client) myStartContainer(cfg ContainerStartConfig) (string, error) {
if c.client == nil {
return "", errors.New(noClientErr)
}
if len(cfg.Image) == 0 {
return "", errors.New(noImageErr)
}
containerConfig := container.Config{
Image: cfg.Image,
}
hostConfig := container.HostConfig{
AutoRemove: true,
}
if cfg.Env != nil {
containerConfig.Env = cfg.Env
}
if cfg.Cmds != nil {
containerConfig.Cmd = make(strslice.StrSlice, len(cfg.Cmds))
for i, _cmd := range cfg.Cmds {
containerConfig.Cmd[i] = _cmd
}
}
if cfg.Volumes != nil {
hostConfig.Mounts = make([]mount.Mount, len(cfg.Volumes))
for i, v := range cfg.Volumes {
hostConfig.Mounts[i] = mount.Mount{
Type: "volume",
Source: v.Source,
Target: v.Target,
}
}
}
var networkingConfig *network.NetworkingConfig
if cfg.Networks != nil {
networkingConfig = &network.NetworkingConfig{EndpointsConfig: map[string]*network.EndpointSettings{}}
for _, nw := range cfg.Networks {
n := nw.Name
networkingConfig.EndpointsConfig[n] = &network.EndpointSettings{Aliases: nw.Aliases}
}
}
cont, err := c.client.ContainerCreate(
c.ctx,
&containerConfig,
&hostConfig,
networkingConfig,
nil,
"server_setup_temporary_container",
)
if err != nil {
return "", err
}
colorlogger.Log.Info("Container ID of "+colorlogger.LYellow, cfg.Image, colorlogger.CLR+" is "+cont.ID)
if err := c.client.ContainerStart(c.ctx, cont.ID, types.ContainerStartOptions{}); err != nil {
return "", err
}
return cont.ID, nil
}
Greater scenario
I'm executing this setup in order to configure the containers which are later executed with 'docker compose' as some of the setups require explicit changes to the containers and can't be done declaratively.
I'm using Golang and trying to take user input from the CLI and send it as a GET request to localhost:8080 but I can't work out how to do it. I currently have the following and am using a Docker container:
func main() {
fmt.Println("Enter desired input: ")
// Get user input
var input string
fmt.Scanln(&input)
http.HandleFunc("/", doSomething)
log.Fatal(http.ListenAndServe(":8081", nil))
resp, err := http.Get("http://localhost:8080/?input=" + input)
// Print response out here
}
func doSomething(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("input")
// Send response
}
Docker file:
FROM golang:1.12.0-alpine3.9
RUN mkdir /app
ADD . /app
WORKDIR /app
RUN go build -o main .
CMD ["/app/main"]
I then start the Docker container:
docker run -it -p 8080:8081 go-app
I would like to enter in numbers in the CLI and then return a response from the HTTP server. How can I do this as the above solution doesn't work (since it doesn't make the GET request within the main() method).
Run the http server in a different goroutine
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
fmt.Println("Enter desired input: ")
// Get user input
var input string
fmt.Scanln(&input)
l, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
go func() {
http.HandleFunc("/", doSomething)
log.Fatal(http.Serve(l, nil))
}()
resp, err := http.Get("http://localhost:8080/?input=" + input)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Resp: %v\n", resp)
// Print response out here
}
func doSomething(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("input")
fmt.Println(query)
// Send response
}
I have a gRPC server
type Server struct {
name string
host string
port string
dbUser string
dbPassword string
dbURL string
dbParameters string
}
func main() {
/*Start of config*/
server := Server{
"User service",
"",
os.Getenv("PORT"),
"",
"",
"",
"",
}
/*End of config*/
log.Printf("Starting: %s RPCServer\n", server.name)
lis, err := net.Listen("tcp", server.host+":"+server.port)
if err != nil {
log.Fatalf("Failed to liste: %v\n", err)
}
defer func() {
err = lis.Close()
if err != nil {
log.Fatalf("Failed to close listener: %v\n", err)
}
}()
gRPCServer := grpc.NewServer()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
reg := codecs.Register(bson.NewRegistryBuilder()).Build()
mongoURI := options.Client().ApplyURI(
"mongodb+srv://" + server.dbUser + ":" + server.dbPassword + "#" + server.dbURL + server.dbParameters,
)
mongoClient, err := mongo.NewClient(mongoURI, &options.ClientOptions{
Registry: reg,
})
if err != nil {
log.Fatalf("unable to create new mongo clinet: %s\n", err)
}
err = mongoClient.Connect(ctx)
if err != nil {
log.Fatalf("unable to connect to db: %s\n", err)
}
/*Start of registering service*/
authService, err := AuthService.NewAuthService()
if err != nil {
log.Fatalf("unable to create server: %s\n", err)
}
s := UserService.NewServer(authService, mongoClient)
UserService.RegisterUserServiceServer(gRPCServer, &s)
/*End of registering service*/
go func() {
if err := gRPCServer.Serve(lis); err != nil {
log.Printf("Failed to serve: %v\n", err)
}
}()
log.Printf("server successfully started on port: %s\n\n", server.port)
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt)
<-c
log.Printf("\nstopping server...\n")
gRPCServer.Stop()
err = lis.Close()
if err != nil {
log.Fatalf("failed to close listner: %s\n", err)
}
err = mongoClient.Disconnect(ctx)
if err != nil {
log.Fatalf("failed to disconnect from mongodb: %s\n", err)
}
log.Printf("successfully closed server\n")
}
And my Dockerfile is
FROM golang:alpine AS build-env
WORKDIR /app
ADD . /app
RUN cd /app && go build Main/server.go
FROM alpine
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
WORKDIR /app
COPY --from=build-env /app/server /app
COPY --from=build-env /app/Config/* /app
ENV GOOGLE_APPLICATION_CREDENTIALS=cred.json
EXPOSE 50051
CMD ["./server"]
I put up the docker image on Google Container Registry, and tried to use Cloud Run to run it using the following command
gcloud run deploy grpc-server-streaming\
--project=project-id\
--platform=managed\
--region=asia-south1\
--image=image-tag\
--allow-unauthenticated
After which I went into Cloud Run application and enabled HTTP/2 connections. But am still not able to connect to the service. I get the following error
2021/04/21 17:04:36 rpc error: code = Unavailable desc = upstream connect error or disconnect/reset before headers. reset reason: connection termination
I've been stuck at this for two days and am not sure what to do.
The same is happening for my ASPNet hosted gRPC service. It was working last Friday (2021-04-30), but on Monday (2021-05-03) I started getting this error message.
I have been searching for days, like you, for an answer. Thank-you to #menghanl for the Envoy pointer, I will open that can of worms.
#Chandraaditya Have a look at your Cloud Run Service's Logs to see if your service is actually being triggered.
I can see my service handling the request at the time I am getting the error. The gRPC service call appears to be 200 OK, but "Envoy" is not sending it back to the client.
so as the title says, I'm trying to execute a simple command inside entrypoint using golang docker sdk (docker api).
func RunBatch(imageName, containerName string, entrypoint []string, volumes []string) string {
ctx := context.Background()
c := getClient()
cfg := &container.Config{Entrypoint: entrypoint, Tty: true, Image: imageName}
hostCfg := &container.HostConfig{Mounts: make([]mount.Mount, len(volumes))}
netCfg := &network.NetworkingConfig{}
startCfg := types.ContainerStartOptions{}
for i := range volumes {
vols := strings.Split(volumes[i], ":")
hostCfg.Mounts[i] = mount.Mount{
Type: mount.TypeBind,
Source: config.Config.BaseDir + vols[0],
Target: vols[1],
}
}
resp, err := c.ContainerCreate(ctx, cfg, hostCfg, netCfg, containerName)
if err != nil {
log.Fatal().Err(err)
}
err = c.ContainerStart(ctx, resp.ID, startCfg)
if err != nil {
log.Fatal().Err(err)
}
_, err = c.ContainerWait(ctx, resp.ID)
if err != nil {
log.Fatal().Err(err)
}
err = c.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{})
if err != nil {
log.Fatal().Err(err)
}
return resp.ID
}
and the entrypoint I'm passing here is ["touch", "/app/$(date +'%T')"]
but the created file looks like $(date +'%T'), I've also tried and failed with ${date +'%T'} and with backqoute as well.
how can I execute those ?!
Unlike the shell form, the exec form does not invoke a command shell. This means that normal shell processing does not happen. For example, ENTRYPOINT [ "echo", "$HOME" ] will not do variable substitution on $HOME. If you want shell processing then either use the shell form or execute a shell directly, for example: ENTRYPOINT [ "sh", "-c", "echo $HOME" ].
RunBatch is going to treat the value of entrypoint literally (as you're experiencing).
You will need to provide it with the Golang equivalent (Time.Format value of (bash's) $(date +%T) to succeed:
Perhaps:
["touch", fmt.Sprintf("/app/%s",time.Now().Format("15:04:05"))]
NOTE the 15:04:05 is the pattern to follow, the value will be the current time
I am trying to create a containerSource for knative service. When I use docker run for the image it gives the output ("or the error from the code"). However when I apply the yaml file then kubectl log shows 'standard_init_linux.go:211: exec user process caused "no such file or directory"'. docker run shows that it is able to find the exec file. So I am not able to understand whats wrong. Someone please guide me through.
my yaml file:
apiVersion: sources.eventing.knative.dev/v1alpha1
kind: ContainerSource
metadata:
labels:
controller-tools.k8s.io: "1.0"
name: cloudevents-source
spec:
image: docker.io/username/pkt-event:latest
args:
- '--debug=true'
sink:
apiVersion: serving.knative.dev/v1alpha1
kind: Service
name: event-display
my go code for the dockerimage is:
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"time"
"github.com/satori/go.uuid"
"knative.dev/eventing-contrib/pkg/kncloudevents"
"encoding/json"
// "io/ioutil"
// "knative.dev/eventing-contrib/vendor/github.com/cloudevents/sdk-go/pkg/cloudevents"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
"github.com/kelseyhightower/envconfig"
)
var (
eventSource string
eventType string
sink string
)
//var u, _ = uuid.NewV4()
var debug = flag.Bool("debug", false, "Enable debug mode (print more information)")
var source = flag.String("source", uuid.NewV4().String(), "Set custom Source for the driver")
func init() {
flag.StringVar(&eventSource, "eventSource", "", "the event-source (CloudEvents)")
flag.StringVar(&eventType, "eventType", "dev.knative.eventing.samples.pkt", "the event-type (CloudEvents)")
flag.StringVar(&sink, "sink", "", "the host url to send pkts to")
}
type envConfig struct {
// Sink URL where to send heartbeat cloudevents
Sink string `envconfig:"SINK"`
}
func main() {
flag.Parse()
var env envConfig
if err := envconfig.Process("", &env); err != nil {
log.Printf("[ERROR] Failed to process env var: %s", err)
os.Exit(1)
}
if env.Sink != "" {
sink = env.Sink
}
if eventSource == "" {
eventSource = fmt.Sprintf("https://knative.dev/eventing-contrib/cmd/heartbeats/#local/demo")
log.Printf("Source: %s", eventSource)
}
client, err := kncloudevents.NewDefaultClient(sink)
if err != nil {
log.Fatalf("failed to create client: %s", err.Error())
}
var period time.Duration
period = time.Duration(1) * time.Second
ticker := time.NewTicker(period)
for {
content := "Send data"
data, err := json.Marshal(content)
if err != nil {
fmt.Println(err)
}
event := cloudevents.Event{
Context: cloudevents.EventContextV02{
Type: "packet.invoke",
Source: *types.ParseURLRef(eventSource),
/*Extensions: map[string]interface{}{
"the": 42,
"heart": "yes",
"beats": true,
},*/
}.AsV02(),
Data: data,
}
if *debug{
log.Printf("Sending event %v", event)
} else {
if _, err := client.Send(context.TODO(), event); err != nil {
log.Printf("failed to send cloudevent: %s", err.Error())
}
}
<-ticker.C
}
}
And Dockerfile is:
FROM golang:1.12 as builder
RUN go version
WORKDIR ${GOPATH}/src/Event-driver
COPY ./ ./
RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
##RUN dep ensure
RUN dep init
RUN dep ensure
RUN CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -v -o my-event my-event.go
RUN pwd && ls
FROM scratch
#FROM ubuntu:disco
COPY --from=builder /go/src/Event-driver/my-event /
ENTRYPOINT ["/my-event"]
That problem occurs because you're trying to run your binary from bash, but scratch has no bash.
I'm normally using alpina instead. To build for alpina you need the same environment variables, so probably you only need to change a second stage image.