diff --git a/src/code.cloudfoundry.org/policy-server/cmd/policy-server-internal/main.go b/src/code.cloudfoundry.org/policy-server/cmd/policy-server-internal/main.go index 4191cfe33..7117958e7 100644 --- a/src/code.cloudfoundry.org/policy-server/cmd/policy-server-internal/main.go +++ b/src/code.cloudfoundry.org/policy-server/cmd/policy-server-internal/main.go @@ -108,6 +108,8 @@ func main() { internalPoliciesHandlerV1 := handlers.NewPoliciesIndexInternal(logger, wrappedStore, policyMapperWriter, errorResponse) + internalPoliciesLastUpdatedHandlerV1 := handlers.NewPoliciesLastUpdatedInternal(logger, wrappedStore, errorResponse) + createTagsHandlerV1 := &handlers.TagsCreate{ Store: wrappedStore, ErrorResponse: errorResponse, @@ -144,13 +146,15 @@ func main() { internalRoutes := rata.Routes{ {Name: "create_tags", Method: "PUT", Path: "/networking/v1/internal/tags"}, {Name: "internal_policies", Method: "GET", Path: "/networking/:version/internal/policies"}, + {Name: "internal_policies_last_updated", Method: "GET", Path: "/networking/:version/internal/policies_last_updated"}, {Name: "internal_security_groups", Method: "GET", Path: "/networking/:version/internal/security_groups"}, } internalHandlers := rata.Handlers{ - "create_tags": metricsWrap("CreateTags", logWrap(createTagsHandlerV1)), - "internal_policies": metricsWrap("InternalPolicies", logWrap(internalPoliciesHandlerV1)), - "internal_security_groups": metricsWrap("InternalSecurityGroups", logWrap(securityGroupsHandlerV1)), + "create_tags": metricsWrap("CreateTags", logWrap(createTagsHandlerV1)), + "internal_policies": metricsWrap("InternalPolicies", logWrap(internalPoliciesHandlerV1)), + "internal_policies_last_updated": metricsWrap("InternalPoliciesLastUpdated", logWrap(internalPoliciesLastUpdatedHandlerV1)), + "internal_security_groups": metricsWrap("InternalSecurityGroups", logWrap(securityGroupsHandlerV1)), } for key, handler := range internalHandlers { diff --git a/src/code.cloudfoundry.org/policy-server/handlers/policies_last_updated_internal.go b/src/code.cloudfoundry.org/policy-server/handlers/policies_last_updated_internal.go new file mode 100644 index 000000000..7cfa129b9 --- /dev/null +++ b/src/code.cloudfoundry.org/policy-server/handlers/policies_last_updated_internal.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "net/http" + "strconv" + + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/policy-server/store" +) + +type PoliciesLastUpdatedInternal struct { + Logger lager.Logger + Store store.Store + ErrorResponse errorResponse +} + +func NewPoliciesLastUpdatedInternal(logger lager.Logger, store store.Store, + errorResponse errorResponse) *PoliciesLastUpdatedInternal { + return &PoliciesLastUpdatedInternal{ + Logger: logger, + Store: store, + ErrorResponse: errorResponse, + } +} + +func (h *PoliciesLastUpdatedInternal) ServeHTTP(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + logger = logger.Session("policies-last-updated-internal") + + lastUpdated, err := h.Store.LastUpdated() + if err != nil { + h.ErrorResponse.InternalServerError(logger, w, err, "database read failed") + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(strconv.Itoa(lastUpdated))) +} diff --git a/src/code.cloudfoundry.org/policy-server/handlers/policies_last_updated_internal_test.go b/src/code.cloudfoundry.org/policy-server/handlers/policies_last_updated_internal_test.go new file mode 100644 index 000000000..9fc085969 --- /dev/null +++ b/src/code.cloudfoundry.org/policy-server/handlers/policies_last_updated_internal_test.go @@ -0,0 +1,89 @@ +package handlers_test + +import ( + "errors" + "net/http" + "net/http/httptest" + + "code.cloudfoundry.org/lager/v3" + "code.cloudfoundry.org/lager/v3/lagertest" + "code.cloudfoundry.org/policy-server/handlers" + "code.cloudfoundry.org/policy-server/handlers/fakes" + storeFakes "code.cloudfoundry.org/policy-server/store/fakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("PoliciesLastUpdatedInternal", func() { + var ( + handler *handlers.PoliciesLastUpdatedInternal + resp *httptest.ResponseRecorder + fakeStore *storeFakes.Store + fakeErrorResponse *fakes.ErrorResponse + logger *lagertest.TestLogger + expectedLogger lager.Logger + expectedResponseBody []byte + ) + + BeforeEach(func() { + expectedResponseBody = []byte("12345") + + fakeStore = &storeFakes.Store{} + fakeStore.LastUpdatedReturns(12345, nil) + logger = lagertest.NewTestLogger("test") + expectedLogger = lager.NewLogger("test").Session("policies-last-updated-internal") + + testSink := lagertest.NewTestSink() + expectedLogger.RegisterSink(testSink) + expectedLogger.RegisterSink(lager.NewWriterSink(GinkgoWriter, lager.DEBUG)) + fakeErrorResponse = &fakes.ErrorResponse{} + handler = &handlers.PoliciesLastUpdatedInternal{ + Logger: logger, + Store: fakeStore, + ErrorResponse: fakeErrorResponse, + } + resp = httptest.NewRecorder() + }) + + It("returns the last updated returned by LastUpdated", func() { + request, err := http.NewRequest("GET", "/networking/v0/internal/policies_last_updated", nil) + Expect(err).NotTo(HaveOccurred()) + MakeRequestWithLogger(handler.ServeHTTP, resp, request, logger) + + Expect(fakeStore.LastUpdatedCallCount()).To(Equal(1)) + Expect(resp.Code).To(Equal(http.StatusOK)) + Expect(resp.Body.Bytes()).To(Equal(expectedResponseBody)) + }) + + Context("when the logger isn't on the request context", func() { + It("still works", func() { + request, err := http.NewRequest("GET", "/networking/v0/internal/policies_last_updated", nil) + Expect(err).NotTo(HaveOccurred()) + handler.ServeHTTP(resp, request) + + Expect(fakeStore.LastUpdatedCallCount()).To(Equal(1)) + Expect(resp.Code).To(Equal(http.StatusOK)) + Expect(resp.Body.Bytes()).To(Equal(expectedResponseBody)) + }) + }) + + Context("when store throws an error", func() { + BeforeEach(func() { + fakeStore.LastUpdatedReturns(0, errors.New("banana")) + }) + + It("calls the internal server error handler", func() { + request, err := http.NewRequest("GET", "/networking/v0/internal/policies_last_updated", nil) + Expect(err).NotTo(HaveOccurred()) + MakeRequestWithLogger(handler.ServeHTTP, resp, request, logger) + + Expect(fakeErrorResponse.InternalServerErrorCallCount()).To(Equal(1)) + + l, w, err, description := fakeErrorResponse.InternalServerErrorArgsForCall(0) + Expect(l).To(Equal(expectedLogger)) + Expect(w).To(Equal(resp)) + Expect(err).To(MatchError("banana")) + Expect(description).To(Equal("database read failed")) + }) + }) +}) diff --git a/src/code.cloudfoundry.org/policy-server/store/fakes/store.go b/src/code.cloudfoundry.org/policy-server/store/fakes/store.go index ae57c06b0..039514177 100644 --- a/src/code.cloudfoundry.org/policy-server/store/fakes/store.go +++ b/src/code.cloudfoundry.org/policy-server/store/fakes/store.go @@ -67,6 +67,18 @@ type Store struct { deleteReturnsOnCall map[int]struct { result1 error } + LastUpdatedStub func() (int, error) + lastUpdatedMutex sync.RWMutex + lastUpdatedArgsForCall []struct { + } + lastUpdatedReturns struct { + result1 int + result2 error + } + lastUpdatedReturnsOnCall map[int]struct { + result1 int + result2 error + } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } @@ -388,6 +400,62 @@ func (fake *Store) DeleteReturnsOnCall(i int, result1 error) { }{result1} } +func (fake *Store) LastUpdated() (int, error) { + fake.lastUpdatedMutex.Lock() + ret, specificReturn := fake.lastUpdatedReturnsOnCall[len(fake.lastUpdatedArgsForCall)] + fake.lastUpdatedArgsForCall = append(fake.lastUpdatedArgsForCall, struct { + }{}) + stub := fake.LastUpdatedStub + fakeReturns := fake.lastUpdatedReturns + fake.recordInvocation("LastUpdated", []interface{}{}) + fake.lastUpdatedMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *Store) LastUpdatedCallCount() int { + fake.lastUpdatedMutex.RLock() + defer fake.lastUpdatedMutex.RUnlock() + return len(fake.lastUpdatedArgsForCall) +} + +func (fake *Store) LastUpdatedCalls(stub func() (int, error)) { + fake.lastUpdatedMutex.Lock() + defer fake.lastUpdatedMutex.Unlock() + fake.LastUpdatedStub = stub +} + +func (fake *Store) LastUpdatedReturns(result1 int, result2 error) { + fake.lastUpdatedMutex.Lock() + defer fake.lastUpdatedMutex.Unlock() + fake.LastUpdatedStub = nil + fake.lastUpdatedReturns = struct { + result1 int + result2 error + }{result1, result2} +} + +func (fake *Store) LastUpdatedReturnsOnCall(i int, result1 int, result2 error) { + fake.lastUpdatedMutex.Lock() + defer fake.lastUpdatedMutex.Unlock() + fake.LastUpdatedStub = nil + if fake.lastUpdatedReturnsOnCall == nil { + fake.lastUpdatedReturnsOnCall = make(map[int]struct { + result1 int + result2 error + }) + } + fake.lastUpdatedReturnsOnCall[i] = struct { + result1 int + result2 error + }{result1, result2} +} + func (fake *Store) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() @@ -401,6 +469,8 @@ func (fake *Store) Invocations() map[string][][]interface{} { defer fake.createMutex.RUnlock() fake.deleteMutex.RLock() defer fake.deleteMutex.RUnlock() + fake.lastUpdatedMutex.RLock() + defer fake.lastUpdatedMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/src/code.cloudfoundry.org/policy-server/store/metrics_wrapper.go b/src/code.cloudfoundry.org/policy-server/store/metrics_wrapper.go index ce4cecbda..7c3d52433 100644 --- a/src/code.cloudfoundry.org/policy-server/store/metrics_wrapper.go +++ b/src/code.cloudfoundry.org/policy-server/store/metrics_wrapper.go @@ -55,6 +55,19 @@ func (mw *MetricsWrapper) Delete(policies []Policy) error { return err } +func (mw *MetricsWrapper) LastUpdated() (int, error) { + startTime := time.Now() + timestamp, err := mw.Store.LastUpdated() + lastUpdatedTimeDuration := time.Now().Sub(startTime) + if err != nil { + mw.MetricsSender.IncrementCounter("StoreLastUpdatedError") + mw.MetricsSender.SendDuration("StoreLastUpdatedErrorTime", lastUpdatedTimeDuration) + } else { + mw.MetricsSender.SendDuration("StoreLastUpdatedSuccessTime", lastUpdatedTimeDuration) + } + return timestamp, err +} + func (mw *MetricsWrapper) Tags() ([]Tag, error) { startTime := time.Now() tags, err := mw.TagStore.Tags() diff --git a/src/code.cloudfoundry.org/policy-server/store/metrics_wrapper_test.go b/src/code.cloudfoundry.org/policy-server/store/metrics_wrapper_test.go index 0c3d72be4..81f67c971 100644 --- a/src/code.cloudfoundry.org/policy-server/store/metrics_wrapper_test.go +++ b/src/code.cloudfoundry.org/policy-server/store/metrics_wrapper_test.go @@ -295,6 +295,46 @@ var _ = Describe("MetricsWrapper", func() { }) }) + Describe("LastUpdated", func() { + BeforeEach(func() { + fakeStore.LastUpdatedReturns(12345, nil) + }) + + It("calls LastUpdated on the Store", func() { + timestamp, err := metricsWrapper.LastUpdated() + Expect(err).NotTo(HaveOccurred()) + Expect(timestamp).To(Equal(12345)) + + Expect(fakeStore.LastUpdatedCallCount()).To(Equal(1)) + }) + + It("emits a metric", func() { + _, err := metricsWrapper.LastUpdated() + Expect(err).NotTo(HaveOccurred()) + + Expect(fakeMetricsSender.SendDurationCallCount()).To(Equal(1)) + name, _ := fakeMetricsSender.SendDurationArgsForCall(0) + Expect(name).To(Equal("StoreLastUpdatedSuccessTime")) + }) + + Context("when there is an error", func() { + BeforeEach(func() { + fakeStore.LastUpdatedReturns(0, errors.New("banana")) + }) + It("emits an error metric", func() { + _, err := metricsWrapper.LastUpdated() + Expect(err).To(MatchError("banana")) + + Expect(fakeMetricsSender.IncrementCounterCallCount()).To(Equal(1)) + Expect(fakeMetricsSender.IncrementCounterArgsForCall(0)).To(Equal("StoreLastUpdatedError")) + + Expect(fakeMetricsSender.SendDurationCallCount()).To(Equal(1)) + name, _ := fakeMetricsSender.SendDurationArgsForCall(0) + Expect(name).To(Equal("StoreLastUpdatedErrorTime")) + }) + }) + }) + Describe("Tags", func() { BeforeEach(func() { fakeTagStore.TagsReturns(tags, nil) diff --git a/src/code.cloudfoundry.org/policy-server/store/migrations/migrations.go b/src/code.cloudfoundry.org/policy-server/store/migrations/migrations.go index abf89ba69..e20204bfe 100644 --- a/src/code.cloudfoundry.org/policy-server/store/migrations/migrations.go +++ b/src/code.cloudfoundry.org/policy-server/store/migrations/migrations.go @@ -412,4 +412,12 @@ var MigrationsToPerform = PolicyServerMigrations{ Id: "76", Up: migration_v0076, }, + PolicyServerMigration{ + Id: "77", + Up: migration_v0077, + }, + PolicyServerMigration{ + Id: "78", + Up: migration_v0078, + }, } diff --git a/src/code.cloudfoundry.org/policy-server/store/migrations/v0077.go b/src/code.cloudfoundry.org/policy-server/store/migrations/v0077.go new file mode 100644 index 000000000..1cde5ac4d --- /dev/null +++ b/src/code.cloudfoundry.org/policy-server/store/migrations/v0077.go @@ -0,0 +1,20 @@ +package migrations + +// Adding policies information table to store the date +// when policies were last updated + +var migration_v0077 = map[string][]string{ + "mysql": { + `CREATE TABLE IF NOT EXISTS policies_info ( + id int NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id), + last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + );`, + }, + "postgres": { + `CREATE TABLE IF NOT EXISTS policies_info ( + id SERIAL PRIMARY KEY, + last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + );`, + }, +} diff --git a/src/code.cloudfoundry.org/policy-server/store/migrations/v0078.go b/src/code.cloudfoundry.org/policy-server/store/migrations/v0078.go new file mode 100644 index 000000000..0bf0daadd --- /dev/null +++ b/src/code.cloudfoundry.org/policy-server/store/migrations/v0078.go @@ -0,0 +1,13 @@ +package migrations + +// Adding first record of last updated field to policies +// set to current date + +var migration_v0078 = map[string][]string{ + "mysql": { + `INSERT INTO policies_info (last_updated) VALUES (CURRENT_TIMESTAMP);`, + }, + "postgres": { + `INSERT INTO policies_info (last_updated) VALUES (CURRENT_TIMESTAMP);`, + }, +} diff --git a/src/code.cloudfoundry.org/policy-server/store/policy.go b/src/code.cloudfoundry.org/policy-server/store/policy.go index e7b3a6736..8b20ed1be 100644 --- a/src/code.cloudfoundry.org/policy-server/store/policy.go +++ b/src/code.cloudfoundry.org/policy-server/store/policy.go @@ -1,6 +1,8 @@ package store -import "code.cloudfoundry.org/cf-networking-helpers/db" +import ( + "code.cloudfoundry.org/cf-networking-helpers/db" +) //counterfeiter:generate -o fakes/policy_repo.go --fake-name PolicyRepo . PolicyRepo type PolicyRepo interface { diff --git a/src/code.cloudfoundry.org/policy-server/store/store.go b/src/code.cloudfoundry.org/policy-server/store/store.go index 3bbdc330b..7efb413b9 100644 --- a/src/code.cloudfoundry.org/policy-server/store/store.go +++ b/src/code.cloudfoundry.org/policy-server/store/store.go @@ -6,6 +6,7 @@ import ( "database/sql" "fmt" "strings" + "time" "code.cloudfoundry.org/cf-networking-helpers/db" "code.cloudfoundry.org/policy-server/store/helpers" @@ -23,6 +24,7 @@ type Store interface { Create([]Policy) error All() ([]Policy, error) Delete([]Policy) error + LastUpdated() (int, error) ByGuids([]string, []string, bool) ([]Policy, error) CheckDatabase() error } @@ -60,10 +62,17 @@ func New(dbConnectionPool Database, g GroupRepo, d DestinationRepo, p PolicyRepo } func (s *store) Create(policies []Policy) error { + if len(policies) == 0 { + return nil + } tx, err := s.conn.Beginx() if err != nil { return fmt.Errorf("create transaction: %s", err) } + err = s.updateLastUpdated(tx) + if err != nil { + return rollback(tx, err) + } err = s.createWithTx(tx, policies) if err != nil { @@ -74,10 +83,17 @@ func (s *store) Create(policies []Policy) error { } func (s *store) Delete(policies []Policy) error { + if len(policies) == 0 { + return nil + } tx, err := s.conn.Beginx() if err != nil { return fmt.Errorf("create transaction: %s", err) } + err = s.updateLastUpdated(tx) + if err != nil { + return rollback(tx, err) + } err = s.deleteWithTx(tx, policies) if err != nil { @@ -87,6 +103,15 @@ func (s *store) Delete(policies []Policy) error { return commit(tx) } +func (s *store) LastUpdated() (int, error) { + var timestamp time.Time + err := s.conn.QueryRow(`SELECT last_updated FROM policies_info LIMIT 1`).Scan(×tamp) + if err != nil { + return 0, fmt.Errorf("getting policies: %s", err) + } + return int(timestamp.Unix()), err +} + func (s *store) CheckDatabase() error { var result int return s.conn.QueryRow("SELECT 1").Scan(&result) @@ -337,3 +362,8 @@ func (s *store) All() ([]Policy, error) { func (s *store) tagIntToString(tag int) string { return fmt.Sprintf("%"+fmt.Sprintf("0%d", s.tagLength*2)+"X", tag) } + +func (s *store) updateLastUpdated(tx db.Transaction) error { + _, err := tx.Exec(`UPDATE policies_info SET last_updated=CURRENT_TIMESTAMP`) + return err +} diff --git a/src/code.cloudfoundry.org/policy-server/store/store_test.go b/src/code.cloudfoundry.org/policy-server/store/store_test.go index 39b7abbfb..c9d348335 100644 --- a/src/code.cloudfoundry.org/policy-server/store/store_test.go +++ b/src/code.cloudfoundry.org/policy-server/store/store_test.go @@ -85,6 +85,7 @@ var _ = Describe("Store", func() { } return err } + It("remains consistent", func() { migrateAndPopulateTags(realDb, 2) dataStore := store.New(realDb, group, destination, policy, 2) @@ -168,16 +169,56 @@ var _ = Describe("Store", func() { Expect(len(p)).To(Equal(2)) }) - Context("when a transaction begin fails", func() { - var err error + It("updates last updated field", func() { + lastUpdatedOriginal, err := dataStore.LastUpdated() + Expect(err).NotTo(HaveOccurred()) + Expect(lastUpdatedOriginal).NotTo(BeNil()) + time.Sleep(1 * time.Second) + + policies := []store.Policy{{ + Source: store.Source{ID: "some-app-guid"}, + Destination: store.Destination{ + ID: "some-app-guid", + Protocol: "tcp", + Ports: store.Ports{ + Start: 8080, + End: 9000, + }, + }, + }, + } + err = dataStore.Create(policies) + Expect(err).NotTo(HaveOccurred()) + + lastUpdatedNew, err := dataStore.LastUpdated() + Expect(err).NotTo(HaveOccurred()) + Expect(lastUpdatedNew).To(BeNumerically(">", lastUpdatedOriginal)) + }) + + Context("when 0 policies passed in", func() { + It("does not update last updated", func() { + lastUpdatedOriginal, err := dataStore.LastUpdated() + Expect(err).NotTo(HaveOccurred()) + Expect(lastUpdatedOriginal).NotTo(BeNil()) + time.Sleep(1 * time.Second) + + err = dataStore.Create([]store.Policy{}) + Expect(err).NotTo(HaveOccurred()) + + lastUpdatedNew, err := dataStore.LastUpdated() + Expect(err).NotTo(HaveOccurred()) + Expect(lastUpdatedNew).To(Equal(lastUpdatedOriginal)) + }) + }) + Context("when a transaction begin fails", func() { BeforeEach(func() { mockDb.BeginxReturns(nil, errors.New("some-db-error")) dataStore = store.New(mockDb, group, destination, policy, 2) }) It("returns an error", func() { - err = dataStore.Create(nil) + err := dataStore.Create([]store.Policy{{}}) Expect(err).To(MatchError("create transaction: some-db-error")) }) }) @@ -828,6 +869,43 @@ var _ = Describe("Store", func() { }})) }) + It("updates last updated field", func() { + lastUpdatedOriginal, err := dataStore.LastUpdated() + Expect(err).NotTo(HaveOccurred()) + Expect(lastUpdatedOriginal).NotTo(BeNil()) + time.Sleep(1 * time.Second) + + err = dataStore.Delete([]store.Policy{{ + Source: store.Source{ID: "some-app-guid"}, + Destination: store.Destination{ + ID: "some-other-app-guid", + Protocol: "tcp", + Port: 8080, + }, + }}) + Expect(err).NotTo(HaveOccurred()) + + lastUpdatedNew, err := dataStore.LastUpdated() + Expect(err).NotTo(HaveOccurred()) + Expect(lastUpdatedNew).To(BeNumerically(">", lastUpdatedOriginal)) + }) + + Context("when 0 policies passed in", func() { + It("does not update last updated", func() { + lastUpdatedOriginal, err := dataStore.LastUpdated() + Expect(err).NotTo(HaveOccurred()) + Expect(lastUpdatedOriginal).NotTo(BeNil()) + time.Sleep(1 * time.Second) + + err = dataStore.Delete([]store.Policy{}) + Expect(err).NotTo(HaveOccurred()) + + lastUpdatedNew, err := dataStore.LastUpdated() + Expect(err).NotTo(HaveOccurred()) + Expect(lastUpdatedNew).To(Equal(lastUpdatedOriginal)) + }) + }) + It("deletes the tags if no longer referenced", func() { err := dataStore.Delete([]store.Policy{{ Source: store.Source{ID: "some-app-guid"}, @@ -867,15 +945,13 @@ var _ = Describe("Store", func() { }) Context("when a transaction begin fails", func() { - var err error - BeforeEach(func() { mockDb.BeginxReturns(nil, errors.New("some-db-error")) dataStore = store.New(mockDb, group, destination, policy, 2) }) It("returns an error", func() { - err = dataStore.Delete(nil) + err := dataStore.Delete([]store.Policy{{}}) Expect(err).To(MatchError("create transaction: some-db-error")) }) })