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 }