github.com/cilium/statedb@v0.3.2/reconciler/example/main.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package main
     5  
     6  import (
     7  	"expvar"
     8  	"fmt"
     9  	"io"
    10  	"log/slog"
    11  	"net/http"
    12  	"os"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/spf13/cobra"
    17  	"golang.org/x/time/rate"
    18  
    19  	"github.com/cilium/hive"
    20  	"github.com/cilium/hive/cell"
    21  	"github.com/cilium/hive/job"
    22  	"github.com/cilium/statedb"
    23  	"github.com/cilium/statedb/reconciler"
    24  )
    25  
    26  // This is a simple example of the statedb reconciler. It implements an
    27  // HTTP API for creating and deleting "memos" that are stored on the
    28  // disk.
    29  //
    30  // To run the application:
    31  //
    32  //   $ go run .
    33  //   (ctrl-c to stop)
    34  //
    35  // To create a memo:
    36  //
    37  //   $ curl -d 'hello world' http://localhost:8080/memos/greeting
    38  //   $ cat memos/greeting
    39  //
    40  // To delete a memo:
    41  //
    42  //   $ curl -XDELETE http://localhost:8080/memos/greeting
    43  //
    44  // The application builds on top of the reconciler which retries any failed
    45  // operations and also does periodic "full reconciliation" to prune unknown
    46  // memos and check that the stored memos are up-to-date. To test the resilence
    47  // you can try out the following:
    48  //
    49  //   # Create 'memos/greeting'
    50  //   $ curl -d 'hello world' http://localhost:8080/memos/greeting
    51  //
    52  //   # Make the file read-only and try changing it:
    53  //   $ chmod a-w memos/greeting
    54  //   $ curl -d 'hei maailma' http://localhost:8080/memos/greeting
    55  //   # (You should now see the update operation hitting permission denied)
    56  //
    57  //   # The reconciliation state can be observed in the Table[*Memo]:
    58  //   $ curl -q http://localhost:8080/statedb | jq .
    59  //
    60  //   # Let's give write permissions back:
    61  //   $ chmod u+w memos/greeting
    62  //   # (The update operation should now succeed)
    63  //   $ cat memos/greeting
    64  //   $ curl -s http://localhost:8080/statedb | jq .
    65  //
    66  //   # The full reconciliation runs every 10 seconds. We can see it in
    67  //   # action by modifying the contents of our greeting or by creating
    68  //   # a file directly:
    69  //   $ echo bogus > memos/bogus
    70  //   $ echo mangled > memos/greeting
    71  //   # (wait up to 10 seconds)
    72  //   $ cat memos/bogus
    73  //   $ cat memos/greeting
    74  //
    75  
    76  func main() {
    77  	cmd := cobra.Command{
    78  		Use: "example",
    79  		Run: func(_ *cobra.Command, args []string) {
    80  			if err := Hive.Run(slog.Default()); err != nil {
    81  				fmt.Fprintf(os.Stderr, "Run: %s\n", err)
    82  			}
    83  		},
    84  	}
    85  
    86  	// Register command-line flags. Currently only
    87  	// has --directory for specifying where to store
    88  	// the memos.
    89  	Hive.RegisterFlags(cmd.Flags())
    90  
    91  	// Add the "hive" command for inspecting the object graph:
    92  	//
    93  	//  $ go run . hive
    94  	//
    95  	cmd.AddCommand(Hive.Command())
    96  
    97  	cmd.Execute()
    98  }
    99  
   100  var Hive = hive.NewWithOptions(
   101  	hive.Options{
   102  		// Create a named DB handle for each module.
   103  		ModuleDecorators: []cell.ModuleDecorator{
   104  			func(db *statedb.DB, id cell.ModuleID) *statedb.DB {
   105  				return db.NewHandle(string(id))
   106  			},
   107  		},
   108  	},
   109  
   110  	statedb.Cell,
   111  	job.Cell,
   112  
   113  	cell.SimpleHealthCell,
   114  
   115  	cell.Provide(reconciler.NewExpVarMetrics),
   116  
   117  	cell.Module(
   118  		"example",
   119  		"Reconciler example",
   120  
   121  		cell.Config(Config{}),
   122  
   123  		cell.Provide(
   124  			// Create and register the RWTable[*Memo]
   125  			NewMemoTable,
   126  
   127  			// Provide the Operations[*Memo] for reconciling Memos.
   128  			NewMemoOps,
   129  		),
   130  
   131  		// Create and register the reconciler for memos.
   132  		// The reconciler watches Table[*Memo] for changes and
   133  		// updates the memo files on disk accordingly.
   134  		cell.Invoke(registerMemoReconciler),
   135  
   136  		cell.Invoke(registerHTTPServer),
   137  	),
   138  )
   139  
   140  func registerMemoReconciler(
   141  	params reconciler.Params,
   142  	ops reconciler.Operations[*Memo],
   143  	tbl statedb.RWTable[*Memo],
   144  	m *reconciler.ExpVarMetrics) error {
   145  
   146  	// Create a new reconciler and register it to the lifecycle.
   147  	// We ignore the returned Reconciler[*Memo] as we don't use it.
   148  	_, err := reconciler.Register(
   149  		params,
   150  		tbl,
   151  		(*Memo).Clone,
   152  		(*Memo).SetStatus,
   153  		(*Memo).GetStatus,
   154  		ops,
   155  		nil, // no batch operations support
   156  
   157  		reconciler.WithMetrics(m),
   158  		// Prune unexpected memos from disk once a minute.
   159  		reconciler.WithPruning(time.Minute),
   160  		// Refresh the memos once a minute.
   161  		reconciler.WithRefreshing(time.Minute, rate.NewLimiter(100.0, 1)),
   162  	)
   163  	return err
   164  }
   165  
   166  func registerHTTPServer(
   167  	lc cell.Lifecycle,
   168  	log *slog.Logger,
   169  	db *statedb.DB,
   170  	memos statedb.RWTable[*Memo]) {
   171  
   172  	mux := http.NewServeMux()
   173  
   174  	// To dump the metrics:
   175  	// curl -s http://localhost:8080/expvar
   176  	mux.Handle("/expvar", expvar.Handler())
   177  
   178  	// For dumping the database:
   179  	// curl -s http://localhost:8080/statedb | jq .
   180  	mux.HandleFunc("/statedb", func(w http.ResponseWriter, r *http.Request) {
   181  		w.Header().Add("Content-Type", "application/json")
   182  		w.WriteHeader(http.StatusOK)
   183  		if err := db.ReadTxn().WriteJSON(w); err != nil {
   184  			panic(err)
   185  		}
   186  	})
   187  
   188  	// For creating and deleting memos:
   189  	// curl -d 'foo' http://localhost:8080/memos/bar
   190  	// curl -XDELETE http://localhost:8080/memos/bar
   191  	mux.HandleFunc("/memos/", func(w http.ResponseWriter, r *http.Request) {
   192  		name, ok := strings.CutPrefix(r.URL.Path, "/memos/")
   193  		if !ok {
   194  			w.WriteHeader(http.StatusBadRequest)
   195  			return
   196  		}
   197  
   198  		txn := db.WriteTxn(memos)
   199  		defer txn.Commit()
   200  
   201  		switch r.Method {
   202  		case "POST":
   203  			content, err := io.ReadAll(r.Body)
   204  			if err != nil {
   205  				return
   206  			}
   207  			memos.Insert(
   208  				txn,
   209  				&Memo{
   210  					Name:    name,
   211  					Content: string(content),
   212  					Status:  reconciler.StatusPending(),
   213  				})
   214  			log.Info("Inserted memo", "name", name)
   215  			w.WriteHeader(http.StatusOK)
   216  
   217  		case "DELETE":
   218  			memo, _, ok := memos.Get(txn, MemoNameIndex.Query(name))
   219  			if !ok {
   220  				w.WriteHeader(http.StatusNotFound)
   221  				return
   222  			}
   223  			memos.Delete(txn, memo)
   224  			log.Info("Deleted memo", "name", name)
   225  			w.WriteHeader(http.StatusOK)
   226  		}
   227  	})
   228  
   229  	server := http.Server{
   230  		Addr:    "127.0.0.1:8080",
   231  		Handler: mux,
   232  	}
   233  
   234  	lc.Append(cell.Hook{
   235  		OnStart: func(cell.HookContext) error {
   236  			log.Info("Serving API", "address", server.Addr)
   237  			go server.ListenAndServe()
   238  			return nil
   239  		},
   240  		OnStop: func(ctx cell.HookContext) error {
   241  			return server.Shutdown(ctx)
   242  		},
   243  	})
   244  
   245  }