service

package module
v0.1.14 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 13, 2025 License: MIT Imports: 36 Imported by: 0

README

Service Library for Go

service is a Go library built to streamline the development of services on Google Cloud Platform. By minimizing boilerplate code and automating much of the setup, it allows you to focus on your business logic without getting bogged down in infrastructure details. The result? A clean, readable main.go file that acts like an index, offering a clear and concise overview of your service — self-documenting and easy to maintain. Whether you're setting up authentication, Pub/Sub, or handling Cloud Tasks, this library is designed to make your services simple, scalable, and production-ready from the start.

Features

  • Simplified Initialization: Load environment variables and configurations with ease, handling both local development and production environments gracefully.
  • Streamlined Service Setup: Quickly set up common GCP services like Cloud SQL, Pub/Sub, and authentication with minimal code.
  • Dependency Injection: Access shared resources like databases, logs, and Google Cloud clients through dependency injection, promoting cleaner code and easier testing.
  • Clean Entry Point: Keep your main.go file concise and readable, resembling an index that outlines the service's structure.
  • Built-in Middleware: Includes authentication and authorization middleware, request handling, and error management out of the box.
  • Production-Ready: Designed with production best practices, including graceful shutdowns, context handling, and proper error logging.

Getting Started

Installation
go get github.com/albeebe/service
Basic Usage

Here's how you can set up a simple service using this library:

package main

import (
    "net/http"

    "github.com/albeebe/service"
)

// All the environment variables are defined here
type EnvVars struct {
  GCP_PROJECT_ID       string `default:"my-project"`
  SERVICE_ACCOUNT      string `default:"my-service@my-project.iam.gserviceaccount.com"`
  CLOUD_SQL_CONNECTION string `default:"my-service:us-east4:shared"`
  CLOUD_SQL_DATABASE   string `default:"my-database"`
  CLOUD_SQL_USER       string `default:"my-user"`
  HOST                 string `default:":8080"`
}

func main() {

  // Load all the environment variables and confirm they're set
  env := EnvVars{}
  if err := service.Initialize(&env); err != nil {
    panic(err)
  }

  // Create the service
  s, err := service.New("my-service", service.Config{
    CloudSQLConnection: env.CLOUD_SQL_CONNECTION,
    CloudSQLDatabase:   env.CLOUD_SQL_DATABASE,
    CloudSQLUser:       env.CLOUD_SQL_USER,
    GCPProjectID:       env.GCP_PROJECT_ID,
    Host:               env.HOST,
    ServiceAccount:     env.SERVICE_ACCOUNT,
  })
  if err != nil {
    panic(fmt.Errorf("failed to create service with err: %s", err.Error()))
  }

  // Add an auth provider to handle authentication
  s.AddAuthProvider(authprovider.New(s))

  // Add endpoints that do not require authentication
  s.AddPublicEndpoint("GET", "/", endpoints.GetRoot)

  // Add endpoints that require authentication
  s.AddAuthenticatedEndpoint("GET", "/authenticated", endpoints.GetAuthenticated)
  s.AddAuthenticatedEndpoint("GET", "/role", endpoints.GetRole, auth.AnyRole("viewer", "editor"))
  s.AddAuthenticatedEndpoint("GET", "/permissions", endpoints.GetPermissions, auth.AllPermissions("project.create"))

  // Add websocket endpoints
  s.AddWebsocket("/websocket", endpoints.Websocket)
  
  // Add endpoints that only services can access
  s.AddServiceEndpoint("GET", "/service", endpoints.GetService)

  // Add endpoints for Pub/Sub subscriptions
  s.AddPubSubEndpoint("_pubsub/demo", pubsub.Demo)

  // Add endpoints for Cloud Tasks
  s.AddCloudTaskEndpoint("/_tasks/demo", tasks.Demo)

  // Add endpoints for Cloud Scheduler
  s.AddCloudSchedulerEndpoint("_scheduled/demo", scheduled.Demo)

  // Begin accepting requests. Blocks until the service terminates.
  s.Run(service.State{
    Starting: func() {
      s.Log.Info("Service starting...")
    },
    Running: func() {
      s.Log.Info("Service running...")
    },
    Terminating: func(err error) {
      if err != nil {
        s.Log.Info("Service terminating with error...", slog.Any("error", err))
      } else {
        s.Log.Info("Service terminating...")
      }
    },
  })
}
Explanation
  • Configuration: Define all necessary configurations in one place.
  • Service Initialization: Initialize your service with service.New, handling all setup internally.
  • Dependency Injection: Access shared resources like the database (s.DB), logger (s.Log), and Google Cloud clients directly from the service instance.
  • Adding Endpoints: Use methods like AddPublicEndpoint and AddAuthenticatedEndpoint to register handlers.
  • Running the Service: Call Run with lifecycle callbacks for starting, running, and terminating states.

Detailed Features

Simplified Initialization

The Initialize function loads environment variables based on a provided specification. It prompts for missing variables during local development and ensures all required variables are set in production.

func Initialize(spec interface{}) error
Service Creation

Create a new service instance with New, which handles configuration validation and sets up GCP credentials.

func New(serviceName string, config Config) (*Service, error)
Dependency Injection

The library utilizes dependency injection to provide access to shared resources throughout your application. This includes:

  • Database Connection (s.DB): Access your Cloud SQL database connection.
  • Logger (s.Log): Use the built-in structured logger for consistent logging.
  • Google Cloud Clients: Access clients for services like Pub/Sub, Cloud Storage, and Cloud Tasks.
  • Context (s.Context): Use the service's context for cancellation and timeout handling.

This approach promotes cleaner code by avoiding global variables and making it easier to write unit tests.

Clean main.go

By abstracting away the boilerplate, your main.go remains clean and self-documenting, making it easy to understand the service structure at a glance.

Adding Endpoints
  • Public Endpoints: For handlers that don't require authentication.

    func (s *Service) AddPublicEndpoint(method, path string, handler func(*Service, *http.Request) *HTTPResponse)
    
  • Authenticated Endpoints: For handlers that require authentication and optional authorization.

    func (s *Service) AddAuthenticatedEndpoint(method, path string, handler func(*Service, *http.Request) *HTTPResponse, authRequirements ...auth.AuthRequirements)
    
  • Websocket Endpoints: For handlers that want to automatically upgrade HTTP requests to WebSocket connections.

    func (s *Service) AddWebsocketEndpoint(relativePath string, handler func(*Service, *websocket.Conn))
    
  • Service Endpoints: For internal service-to-service communication with strict authentication.

    func (s *Service) AddServiceEndpoint(method, path string, handler func(*Service, *http.Request) *HTTPResponse, authRequirements ...auth.AuthRequirements)
    
  • Cloud Task Endpoints: Specifically for handling Cloud Tasks.

    func (s *Service) AddCloudTaskEndpoint(path string, handler func(*Service, *http.Request) error)
    
  • Cloud Scheduler Endpoints: For handling scheduled tasks via Cloud Scheduler.

    func (s *Service) AddCloudSchedulerEndpoint(path string, handler func(*Service, *http.Request) error)
    
  • Pub/Sub Endpoints: For processing Pub/Sub messages.

    func (s *Service) AddPubSubEndpoint(path string, handler func(*Service, PubSubMessage) error)
    
Authentication and Authorization

Set up authentication providers and middleware effortlessly.

func (s *Service) AddAuthProvider(authProvider auth.AuthProvider) error
Accessing Shared Resources

Access various clients and utilities provided by the service instance:

  • Database Client:

    db := s.DB
    
  • Logger:

    s.Log.Info("Logging information.")
    
  • Pub/Sub Client:

    messageID, err := s.PublishToPubSub("topic-name", messageData)
    
  • Cloud Storage Client:

    storageClient := s.CloudStorageClient
    
  • Cloud Tasks Client:

    tasksClient := s.CloudTasksClient
    
Graceful Shutdown

Handles OS signals and context cancellations to terminate the service gracefully.

Utility Functions
  • Response Helpers:

    func Text(statusCode int, text string) *HTTPResponse
    func JSON(statusCode int, obj interface{}) *HTTPResponse
    
  • Request Parsing:

    func UnmarshalJSONBody(r *http.Request, target interface{}) error
    
Example with Dependency Injection
func endpointHandler(s *service.Service, r *http.Request) *service.HTTPResponse {
    // Access the database
    db := s.DB
    // Use the database connection
    rows, err := db.Query("SELECT * FROM data_table")
    if err != nil {
        s.Log.Error("Database query failed.", err)
        return service.InternalServerError()
    }
    defer rows.Close()

    // Process data...
    data := []DataModel{}
    for rows.Next() {
        var item DataModel
        if err := rows.Scan(&item.ID, &item.Value); err != nil {
            s.Log.Error("Row scan failed.", err)
            return service.InternalServerError()
        }
        data = append(data, item)
    }

    // Log the operation
    s.Log.Info("Data retrieved successfully.")

    // Return the response
    return service.JSON(http.StatusOK, data)
}

License

This project is licensed under the MIT License. See the LICENSE file for details.

Contributing

Contributions are welcome! Feel free to open an issue or submit a pull request with any proposed changes.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Initialize

func Initialize(spec interface{}) error

Initialize loads the environment variables specified in the provided spec struct. In a local development environment, if any variables are missing, the user is prompted to enter the missing values. In a production environment, if required variables are not set, the function returns an error, indicating that the configuration is incomplete and the service should not start until the issue is resolved.

func ParseClaimsFromRequest

func ParseClaimsFromRequest(r *http.Request, claims interface{}) error

ParseClaimsFromRequest extracts the JWT from the Authorization header of the request, decodes the payload, and unmarshals it into the provided claims struct WITHOUT VERIFYING THE SIGNATURE.

func ParsePubSubEnvelope added in v0.1.10

func ParsePubSubEnvelope(r *http.Request) ([]byte, string, time.Time, error)

ParsePubSubEnvelope extracts and decodes the Pub/Sub message from an incoming HTTP request.

This function expects a JSON payload containing a Pub/Sub message envelope. It performs the following steps: 1. Decodes the JSON request body into a structured envelope. 2. Extracts the base64-encoded message data, message ID, and publish timestamp. 3. Returns the decoded data, message ID, and publish timestamp. If an error occurs at any step, it is returned.

Parameters:

  • r *http.Request: The incoming HTTP request containing the Pub/Sub message.

Returns:

  • []byte: The decoded message data.
  • string: The unique Pub/Sub message ID.
  • time.Time: The timestamp when the message was published.
  • error: An error if JSON decoding or base64 decoding fails, otherwise nil.

func UnmarshalJSONBody

func UnmarshalJSONBody(r *http.Request, target interface{}) error

UnmarshalJSONBody reads the JSON-encoded body of an HTTP request and unmarshals it into the provided target. It returns an error if the request body is empty or if the JSON decoding fails.

Types

type Config

type Config struct {
	CloudSQLConnection string // Cloud SQL instance connection string in the format "project:region:instance"
	CloudSQLDatabase   string // Name of the specific database within the Cloud SQL instance
	CloudSQLUser       string // Username for accessing the Cloud SQL database
	GCPProjectID       string // Google Cloud Platform Project ID where the service is deployed
	Host               string // The host address where the service listens for incoming requests (e.g., ":8080")
	ServiceAccount     string // Service account email used for authentication with GCP resources
}

type EndpointHandler

type EndpointHandler func(*Service, *http.Request) *HTTPResponse

type HTTPResponse

type HTTPResponse struct {
	StatusCode int           // The HTTP status code of the response (e.g., 200, 404)
	Headers    http.Header   // The headers of the HTTP response (e.g., Content-Type, Set-Cookie)
	Body       io.ReadCloser // The response body, allowing streaming of the content and efficient memory usage
}

func InternalServerError

func InternalServerError() *HTTPResponse

InternalServerError returns an HTTP 500 response with a standard "internal server error" message.

func JSON

func JSON(statusCode int, obj interface{}) *HTTPResponse

JSON sets the HTTP response with the provided status code and a JSON-encoded body generated from the provided object. If an error occurs during the JSON encoding process (e.g., unsupported types or invalid data), the function gracefully handles it by setting the response body to `null`. The response is streamed using a pipe to avoid loading the entire JSON payload into memory at once, making it suitable for handling large objects.

func Text

func Text(statusCode int, text string) *HTTPResponse

Text sets the HTTP response with the provided status code and plain text body.

func Textf

func Textf(statusCode int, text string, args ...any) *HTTPResponse

Textf formats a string with the provided arguments and sets the HTTP response with the given status code and the formatted plain text body. It is a variant of the Text function that supports formatted text using fmt.Sprintf.

type PubSubHandler

type PubSubHandler func(*Service, PubSubMessage) error

type PubSubMessage

type PubSubMessage struct {
	ID        string    `json:"id"`        // Unique identifier for the message.
	Published time.Time `json:"published"` // Time the message was published.
	Data      []byte    `json:"data"`      // Data payload of the message as a byte slice.
}

type Service

type Service struct {
	Context            context.Context
	CloudStorageClient *storage.Client
	CloudTasksClient   *cloudtasks.Client
	GoogleCredentials  *google.Credentials
	IAMClient          *credentials.IamCredentialsClient
	DB                 *sql.DB
	Log                *slog.Logger
	Name               string
	// contains filtered or unexported fields
}

func New

func New(serviceName string, config Config) (*Service, error)

New initializes a new service instance with a service name, and configuration. It validates the configuration, sets up Google Cloud credentials, and prepares the service for use. Returns a configured Service or an error on failure.

func (*Service) AddAuthenticatedEndpoint

func (s *Service) AddAuthenticatedEndpoint(method, relativePath, permission string, handler EndpointHandler)

AddAuthenticatedEndpoint registers an HTTP endpoint that requires authentication.

Optionally, a permission can be specified and is checked after authentication. If the permission requirements is not met, a 403 Forbidden response is returned.

If the service was initialized without an AuthProvider, it logs a fatal error and exits. If authentication fails, a 401 Unauthorized response is returned. If authorization requirements are provided and the request fails authorization, a 403 Forbidden response is returned. In case of an internal error during processing, a 500 Internal Server Error is returned.

func (*Service) AddCloudSchedulerEndpoint

func (s *Service) AddCloudSchedulerEndpoint(relativePath string, handler EndpointHandler)

AddCloudSchedulerEndpoint registers a new POST endpoint at the specified relativePath to handle incoming Google Cloud Scheduler requests. In production, it verifies the authenticity of the request, while in local or non-production environments, request verification is skipped.

func (*Service) AddCloudTaskEndpoint

func (s *Service) AddCloudTaskEndpoint(relativePath string, handler EndpointHandler)

AddCloudTaskEndpoint registers a new POST endpoint at the specified relativePath to handle incoming Google Cloud Tasks. In production, it verifies the authenticity of the request, while in local or non-production environments, request verification is skipped.

func (*Service) AddPubSubEndpoint

func (s *Service) AddPubSubEndpoint(relativePath string, handler EndpointHandler)

AddPubSubEndpoint registers a new POST endpoint at the specified relativePath to handle incoming Pub/Sub messages. In production, it verifies the authenticity of the request, while in local or non-production environments, request verification is skipped.

func (*Service) AddPublicEndpoint

func (s *Service) AddPublicEndpoint(method, relativePath string, handler EndpointHandler)

AddPublicEndpoint registers a new HTTP endpoint with the specified method (e.g., "GET", "POST") and relative path. It wraps the provided handler function so that the current Service instance is passed into the handler when the endpoint is invoked. This endpoint does not require authentication. If an error occurs while registering the endpoint, the function will log the error and terminate the program.

func (*Service) AddServiceEndpoint

func (s *Service) AddServiceEndpoint(method, relativePath, permission string, handler EndpointHandler)

AddServiceEndpoint registers an HTTP endpoint that requires authentication and is restricted to service requests only. It first authenticates the request, ensuring that only valid credentials are allowed, and then verifies that the request comes specifically from a trusted service.

If the service was initialized without an AuthProvider, it logs a fatal error and exits. If authentication fails, a 401 Unauthorized response is returned. If the request is not verified as coming from a service, a 403 Forbidden response is returned indicating that access is restricted to services.

Optionally, a permission can be specified and is checked after authentication. If the permission requirements is not met, a 403 Forbidden response is returned.

The handler function receives the Service instance and the HTTP request, and returns an HTTPResponse. In case of an internal error during processing, a 500 Internal Server Error is returned. This endpoint is intended for use by other services and ensures only authenticated and verified service requests are permitted.

func (*Service) AddWebsocketEndpoint

func (s *Service) AddWebsocketEndpoint(relativePath string, handler WebsocketHandler)

AddWebsocketEndpoint registers a WebSocket handler at the specified relative path, handling the WebSocket upgrade process and connection lifecycle. It wraps the provided WebsocketHandler function with middleware to upgrade HTTP requests to WebSocket connections, and automatically closes the connection when the handler completes.

func (*Service) AuthClient

func (s *Service) AuthClient() (*http.Client, error)

AuthClient returns an *http.Client that automatically attaches JWT tokens to requests and refreshes them as needed. It requires the service to have been initialized with an AuthProvider.

func (*Service) AuthenticateRequest added in v0.1.1

func (s *Service) AuthenticateRequest(r *http.Request, permission string) (bool, error)

AuthenticateRequest validates an HTTP request by performing both authentication and authorization checks using the AuthProvider configured for the service. It ensures the request is authenticated and then verifies that it meets the specified authorization requirement.

Parameters: - r: The HTTP request to be authenticated and authorized. - permission: Name of a permission the client is required to have.

Returns: - A boolean indicating whether the request is successfully authenticated and authorized. - An error if something goes wrong.

Notes: - Uses the service's configured AuthProvider to perform all checks.

func (*Service) Config

func (s *Service) Config() *Config

Config returns the current configuration of the service. It provides access to the internal configuration stored in the service.

func (*Service) CreateCloudTask added in v0.1.9

func (s *Service) CreateCloudTask(queue, callbackURL string, body []byte, delay, timeout time.Duration) error

CreateCloudTask creates and schedules a new task in the specified Cloud Tasks queue.

The task is configured as an HTTP request with a POST method, sending the provided request body to the given callback URL. It also includes an OIDC token for authentication.

Parameters:

  • queue: The fully qualified name of the Cloud Tasks queue where the task will be created.
  • callbackURL: The URL that will receive the HTTP request when the task is executed.
  • body: The request payload to be sent in the task's HTTP request body.
  • delay: The duration to wait before the task is executed (schedules the task in the future).
  • timeout: The maximum duration allowed for the task to execute before it times out.

Returns:

  • error: An error if the task creation fails, otherwise nil.

The function uses the CloudTasksClient to create a new task with the specified parameters. The task is authenticated using an OIDC token associated with the configured service account.

func (*Service) GenerateGoogleIDToken

func (s *Service) GenerateGoogleIDToken(audience string) (string, error)

GenerateGoogleIDToken generates a Google ID token for a given audience. It uses a service account to create the token, either by impersonating the account in non-production environments or by querying the metadata server in production.

func (*Service) IsRequestFromService added in v0.1.2

func (s *Service) IsRequestFromService(r *http.Request) bool

IsRequestFromService checks whether the given HTTP request originates from a service. It delegates the request to the underlying AuthProvider to perform the service request check.

func (*Service) PublishToPubSub

func (s *Service) PublishToPubSub(topic string, message interface{}) (string, error)

PublishToPubSub sends a message to the specified Pub/Sub topic. It returns the message ID or an error if the operation fails.

func (*Service) Run

func (s *Service) Run(state State)

Run starts the service and blocks, waiting for an OS signal, context cancellation, or an error. Lifecycle callbacks from the State struct are invoked at each stage: - `Starting`: Called when the service starts. - `Running`: Called when the service is running. - `Terminating`: Called during shutdown, with an error if one triggered the termination.

The function returns only after the service has gracefully shut down.

func (*Service) SetAuthProvider added in v0.1.1

func (s *Service) SetAuthProvider(authProvider auth.AuthProvider) error

SetAuthProvider initializes the authentication provider for the service.

func (*Service) Shutdown

func (s *Service) Shutdown()

Shutdown initiates an immediate graceful shutdown by canceling the service's context, signaling all components to stop their operations. This method triggers the shutdown process but does not block or wait for the service to fully stop.

type State

type State struct {
	Starting    func()          // Called when the service is starting
	Running     func()          // Called when the service is running
	Terminating func(err error) // Called when the service is terminating, with an optional error if it was due to a failure
}

type WebsocketHandler

type WebsocketHandler func(*Service, *websocket.Conn)

Directories

Path Synopsis
pkg

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL