// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

syntax = "proto3";

// For more information, see: https://github.com/hashicorp/consul/tree/main/docs/resources
package hashicorp.consul.resource;

import "annotations/ratelimit/ratelimit.proto";
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";

// Type describes a resource's type. It follows the GVK (Group Version Kind)
// [pattern](https://book.kubebuilder.io/cronjob-tutorial/gvks.html) established
// by Kubernetes.
message Type {
  // Group describes the area of functionality to which this resource type
  // relates (e.g. "catalog", "authorization").
  string group = 1;

  // GroupVersion is incremented when sweeping or backward-incompatible changes
  // are made to the group's resource types.
  string group_version = 2;

  // Kind identifies the specific resource type within the group.
  string kind = 3;
}

// Tenancy describes the tenancy units in which the resource resides.
message Tenancy {
  // Partition is the topmost administrative boundary within a cluster.
  // https://developer.hashicorp.com/consul/docs/enterprise/admin-partitions
  //
  // When using the List and WatchList endpoints, provide the wildcard value "*"
  // to list resources across all partitions.
  string partition = 1;

  // Namespace further isolates resources within a partition.
  // https://developer.hashicorp.com/consul/docs/enterprise/namespaces
  //
  // When using the List and WatchList endpoints, provide the wildcard value "*"
  // to list resources across all namespaces.
  string namespace = 2;
}

// ID uniquely identifies a resource.
message ID {
  // Uid is the unique internal identifier we gave to the resource.
  //
  // It is primarily used to tell the difference between the current resource
  // and previous deleted resources with the same user-given name.
  //
  // Concretely, Uid is a [ULID](https://github.com/ulid/spec) and you can treat
  // its timestamp component as the resource's creation time.
  string uid = 1;

  // Name is the user-given name of the resource (e.g. the "billing" service).
  string name = 2;

  // Type identifies the resource's type.
  Type type = 3;

  // Tenancy identifies the tenancy units (i.e. partition, namespace) in which
  // the resource resides.
  Tenancy tenancy = 4;
}

// Resource describes a resource of a known type managed by Consul.
message Resource {
  // ID uniquely identifies the resource.
  ID id = 1;

  // Owner (optionally) describes which resource "owns" this resource, it is
  // immutable and can only be set on resource creation. Owned resources will
  // be automatically deleted when their owner is deleted.
  ID owner = 2;

  // Version is the low-level version identifier used by the storage backend
  // in CAS (Compare-And-Swap) operations. It will change when the resource is
  // modified in any way, including status updates.
  //
  // When calling the Write endpoint, providing a non-blank version will perform
  // a CAS (Compare-And-Swap) write, which will result in an Aborted error code
  // if the given version doesn't match what is stored.
  string version = 3;

  // Generation is incremented whenever the resource's content (i.e. not its
  // status) is modified. You can think of it as being the "user version".
  //
  // Concretely, Generation is a [ULID](https://github.com/ulid/spec) and you
  // can treat its timestamp component as the resource's modification time.
  string generation = 4;

  // Metadata contains key/value pairs of arbitrary metadata about the resource.
  // "deletionTimestamp" and "finalizers" keys are reserved for internal use.
  map<string, string> metadata = 5;

  // Status is used by controllers to communicate the result of attempting to
  // reconcile and apply the resource (e.g. surface semantic validation errors)
  // with users and other controllers. Each status is identified by a unique key
  // and should only ever be updated by one controller.
  //
  // Status can only be updated via the WriteStatus endpoint. Attempting to do
  // so via the Write endpoint will result in an InvalidArgument error code.
  map<string, Status> status = 6;

  // Data contains the resource's type-specific content.
  google.protobuf.Any data = 7;
}

// Status is used by controllers to communicate the result of attempting to
// reconcile and apply a resource (e.g. surface semantic validation errors)
// with users and other controllers.
message Status {
  // ObservedGeneration identifies which generation of a resource this status
  // related to. It can be used to determine whether the current generation of
  // a resource has been reconciled.
  string observed_generation = 1;

  // Conditions contains a set of discreet observations about the resource in
  // relation to the current state of the system (e.g. it is semantically valid).
  repeated Condition conditions = 2;

  // UpdatedAt is the time at which the status was last written.
  google.protobuf.Timestamp updated_at = 3;
}

// Condition represents a discreet observation about a resource in relation to
// the current state of the system.
//
// It is heavily inspired by Kubernetes' [conditions](https://bit.ly/3H9Y6IK)
// and the Gateway API [types and reasons](https://bit.ly/3n2PPiP).
message Condition {
  // State represents the state of the condition (i.e. true/false/unknown).
  enum State {
    // STATE_UNKNOWN means that the state of the condition is unknown.
    //
    // buf:lint:ignore ENUM_ZERO_VALUE_SUFFIX
    STATE_UNKNOWN = 0;

    // STATE_TRUE means that the state of the condition is true.
    STATE_TRUE = 1;

    // STATE_FALSE means that the state of the condition is false.
    STATE_FALSE = 2;
  }

  // Type identifies the type of condition (e.g. "Invalid", "ResolvedRefs").
  string type = 1;

  // State represents the state of the condition (i.e. true/false/unknown).
  State state = 2;

  // Reason provides more machine-readable details about the condition (e.g.
  // "InvalidProtocol").
  string reason = 3;

  // Message contains a human-friendly description of the status.
  string message = 4;

  // Resource identifies which resource this condition relates to, when it is
  // not the core resource itself.
  Reference resource = 5;
}

// Reference identifies which resource a condition relates to, when it is not
// the core resource itself.
message Reference {
  // Type identifies the resource's type.
  Type type = 1;

  // Tenancy identifies the tenancy units (i.e. partition, namespace) in which
  // the resource resides.
  Tenancy tenancy = 2;

  // Name is the user-given name of the resource (e.g. the "billing" service).
  string name = 3;

  // Section identifies which part of the resource the condition relates to.
  string section = 4;
}

// Tombstone represents a promise to delete all of a resource's immediately
// owned (child) resources, if any.
message Tombstone {
  // Owner resource identifier.
  ID owner = 1;
}

// ResourceService provides the shared primitives for storing, querying, and
// watching resources of different types.
//
// It is exposed on our external gRPC port and used internally by controllers.
//
// # Consistency Guarentees
//
// All reads are eventually consistent by default. Concretely, we guarantee
// [monotonic reads](https://jepsen.io/consistency/models/monotonic-reads).
//
// That is, a read will always return results that are as up-to-date as an
// earlier read, provided both happen on the same Consul server. But we do
// not make any such guarantee about writes. In other words, reads won't
// necessarily reflect earlier writes, even when made against the same server.
//
// This guarantee also holds between the Read and WatchList endpoints such that
// you'll never receive an event about a resource that you cannot immediately
// read, provided both the Read and WatchList happen on the same server.
//
// The Read endpoint also supports a strong consistency mode that guarantees
// [linearizability](https://jepsen.io/consistency/models/linearizable), such
// that a read will always return the most up-to-date version of a resource,
// without caveat.
//
// This is much more expensive than eventual consistency and when using the Raft
// storage backend, will increase load on the cluster leader, so should be used
// sparingly.
//
// To opt-in to strongly consistent reads set the `x-consul-consistency-mode`
// gRPC metadata field to "consistent".
service ResourceService {
  // Read a resource by ID.
  //
  // By default, reads are eventually consistent, but you can opt-in to strong
  // consistency via the x-consul-consistency-mode metadata (see ResourceService
  // docs for more info).
  //
  // Errors with NotFound if the resource is not found.
  //
  // Errors with InvalidArgument if the request fails validation or the resource
  // is stored as a type with a different GroupVersion than was requested.
  //
  // Errors with PermissionDenied if the caller is not authorized to read
  // the resource.
  rpc Read(ReadRequest) returns (ReadResponse) {
    option (hashicorp.consul.internal.ratelimit.spec) = {
      operation_type: OPERATION_TYPE_READ,
      operation_category: OPERATION_CATEGORY_RESOURCE
    };
  }

  // Write a resource.
  //
  // To perform a CAS (Compare-And-Swap) write, provide the current resource
  // version in the Resource.Version field. If the given version doesn't match
  // what is currently stored, an Aborted error code will be returned.
  //
  // To perform a blanket write (update regardless of the stored version),
  // provide an empty Version in the Resource.Version field. Note that the
  // write may still fail due to not being able to internally do a CAS write
  // and return an Aborted error code.
  //
  // Resource.Id.Uid can (and by controllers, should) be provided to avoid
  // accidentally modifying a resource if it has been deleted and recreated.
  // If the given Uid doesn't match what is stored, a FailedPrecondition error
  // code will be returned.
  //
  // It is not possible to modify the resource's status using Write. You must
  // use WriteStatus instead.
  rpc Write(WriteRequest) returns (WriteResponse) {
    option (hashicorp.consul.internal.ratelimit.spec) = {
      operation_type: OPERATION_TYPE_WRITE,
      operation_category: OPERATION_CATEGORY_RESOURCE
    };
  }

  // WriteStatus updates one of the resource's statuses. It should only be used
  // by controllers.
  //
  // To perform a CAS (Compare-And-Swap) write, provide the current resource
  // version in the Version field. If the given version doesn't match what is
  // currently stored, an Aborted error code will be returned.
  //
  // Note: in most cases, CAS status updates are not necessary because updates
  // are scoped to a specific status key and controllers are leader-elected so
  // there is no chance of a conflict.
  //
  // Id.Uid must be provided to avoid accidentally modifying a resource if it has
  // been deleted and recreated. If the given Uid doesn't match what is stored,
  // a FailedPrecondition error code will be returned.
  rpc WriteStatus(WriteStatusRequest) returns (WriteStatusResponse) {
    option (hashicorp.consul.internal.ratelimit.spec) = {
      operation_type: OPERATION_TYPE_WRITE,
      operation_category: OPERATION_CATEGORY_RESOURCE
    };
  }

  // List resources of a given type, tenancy, and optionally name prefix.
  //
  // To list resources across all tenancy units, provide the wildcard "*" value.
  //
  // Results are eventually consistent (see ResourceService docs for more info).
  rpc List(ListRequest) returns (ListResponse) {
    option (hashicorp.consul.internal.ratelimit.spec) = {
      operation_type: OPERATION_TYPE_READ,
      operation_category: OPERATION_CATEGORY_RESOURCE
    };
  }

  // List resources of a given owner.
  //
  // Results are eventually consistent (see ResourceService docs for more info).
  rpc ListByOwner(ListByOwnerRequest) returns (ListByOwnerResponse) {
    option (hashicorp.consul.internal.ratelimit.spec) = {
      operation_type: OPERATION_TYPE_READ,
      operation_category: OPERATION_CATEGORY_RESOURCE
    };
  }

  // Delete a resource by ID.
  //
  // Deleting a non-existent resource will return a successful response for
  // idempotency.
  //
  // To perform a CAS (Compare-And-Swap) deletion, provide the current resource
  // version in the Version field. If the given version doesn't match what is
  // currently stored, an Aborted error code will be returned.
  //
  // Resource.Id.Uid can (and by controllers, should) be provided to avoid
  // accidentally modifying a resource if it has been deleted and recreated.
  // If the given Uid doesn't match what is stored, a FailedPrecondition error
  // code will be returned.
  rpc Delete(DeleteRequest) returns (DeleteResponse) {
    option (hashicorp.consul.internal.ratelimit.spec) = {
      operation_type: OPERATION_TYPE_WRITE,
      operation_category: OPERATION_CATEGORY_RESOURCE
    };
  }

  // WatchList watches resources of the given type, tenancy, and optionally name
  // prefix. It returns results for the current state-of-the-world at the start
  // of the stream, and delta events whenever resources are written or deleted.
  //
  // To watch resources across all tenancy units, provide the wildcard "*" value.
  //
  // WatchList makes no guarantees about event timeliness (e.g. an event for a
  // write may not be received immediately), but it does guarantee that events
  // will be emitted in the correct order. See ResourceService docs for more
  // info about consistency guarentees.
  //
  // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME
  rpc WatchList(WatchListRequest) returns (stream WatchEvent) {
    option (hashicorp.consul.internal.ratelimit.spec) = {
      operation_type: OPERATION_TYPE_READ,
      operation_category: OPERATION_CATEGORY_RESOURCE
    };
  }

  // MutateAndValidate a resource.
  //
  // Applies a resource type's registered mutation and validation hooks to
  // a resource. This is useful for filling in defaults and validating inputs before
  // doing a Write. It's not a pre-requisite since the Write endpoint will also apply
  // the hooks.
  rpc MutateAndValidate(MutateAndValidateRequest) returns (MutateAndValidateResponse) {
    option (hashicorp.consul.internal.ratelimit.spec) = {
      operation_type: OPERATION_TYPE_READ,
      operation_category: OPERATION_CATEGORY_RESOURCE
    };
  }
}

// ReadRequest contains the parameters to the Read endpoint.
message ReadRequest {
  // ID of the resource.
  ID id = 1;
}

// ReadResponse contains the results of calling the Read endpoint.
message ReadResponse {
  // Resource that was read.
  Resource resource = 1;
}

// ListRequest contains the parameters to the List endpoint.
message ListRequest {
  // Type of resource to list.
  Type type = 1;

  // Tenancy units in which to list resources. To list resources in all units,
  // provide the wildcard "*" value.
  Tenancy tenancy = 2;

  // NamePrefix filters the results to those with a name beginning with the
  // given prefix.
  string name_prefix = 3;
}

// ListResponse contains the results of calling the List endpoint.
message ListResponse {
  // Resources that were listed.
  repeated Resource resources = 1;
}

// ListByOwnerRequest contains the parameters to the ListByOwner endpoint.
message ListByOwnerRequest {
  ID owner = 1;
}

// ListByOwnerResponse contains the results of calling the ListByOwner endpoint.
message ListByOwnerResponse {
  // Resources that were listed.
  repeated Resource resources = 1;
}

// WriteRequest contains the parameters to the Write endpoint.
message WriteRequest {
  // Resource to write.
  Resource resource = 1;
}

// WriteResponse contains the results of calling the Write endpoint.
message WriteResponse {
  // Resource that was written.
  Resource resource = 1;
}

// WriteStatusRequest contains the parameters to the WriteStatus endpoint.
message WriteStatusRequest {
  // ID of the resource to which the status will be written. Must contain a Uid.
  ID id = 1;

  // Version may be provided to perform a CAS (Compare-And-Swap) update of the
  // status. If the given version doesn't match what is currently stored, an
  // Aborted error code will be returned.
  //
  // Note: in most cases, CAS status updates are not necessary because updates
  // are scoped to a specific status key and controllers are leader-elected so
  // there is no chance of a conflict.
  string version = 2;

  // Key identifies which status will be written. Generally, each controller
  // should write 1 status which it owns exclusively (i.e. no other controller
  // updates it).
  string key = 3;

  // Status that will be written to the resource.
  Status status = 4;
}

// WriteStatusResponse contains the results of calling the WriteStatus endpoint.
message WriteStatusResponse {
  // Resource to which the status was written.
  Resource resource = 1;
}

// DeleteRequest contains the parameters to the Delete endpoint.
message DeleteRequest {
  // ID of the resource that will be deleted.
  ID id = 1;

  // Version may be provided to perform a CAS (Compare-And-Swap) deletion of the
  // resource. If the given version doesn't match what is currently stored, an
  // Aborted error code will be returned.
  string version = 2;
}

// DeleteResponse contains the results of calling the Delete endpoint.
message DeleteResponse {}

// WatchListRequest contains the parameters to the WatchList endpoint.
message WatchListRequest {
  // Type of resource to watch.
  Type type = 1;

  // Tenancy units in which to watch resources. To list resources in all units,
  // provide the wildcard "*" value.
  Tenancy tenancy = 2;

  // NamePrefix filters the results to those with a name beginning with the
  // given prefix.
  string name_prefix = 3;
}

// WatchEvent is emitted on the WatchList stream when a resource changes.
message WatchEvent {
  // Upsert indicates that the resource was written (i.e. created or
  // updated). All events from the initial state-of-the-world will be upsert
  // events.
  message Upsert {
    Resource resource = 1;
  }

  // Delete indicates that the resource was deleted.
  message Delete {
    Resource resource = 1;
  }

  // EndOfSnapshot is sent to indicate that the initial snapshot events have
  // been sent and future events will modify that set.
  message EndOfSnapshot {}

  oneof event {
    Upsert upsert = 1;
    Delete delete = 2;
    EndOfSnapshot end_of_snapshot = 3;
  }
}

// MutateAndValidateRequest contains the parameters to the MutateAndValidate endpoint.
message MutateAndValidateRequest {
  Resource resource = 1;
}

// MutateAndValidateResponse contains the results of calling the MutateAndValidate endpoint.
message MutateAndValidateResponse {
  Resource resource = 1;
}
