# Watching Unowned Resources

You may want your app to watch for changes to resources managed by another app. This is possible in a few ways, 
but we'll explore only the ways to do so in `simple.App` in this document 
(if building an `App`-implementation from scratch, see how operators are constructed with the `operator` package).

There are essentially two ways to approach using another app's kinds in your app: importing the kind definition from the other app's repository, 
or working with it in an Untyped (or partly-typed) manner. 

## Importing Another App's Kind Information

The easiest, if available, is to import the kind information from the other app's repository, provided the app is built 
using the grafana-app-sdk. In that case, you can use the package with the kind code you desire, and import 
and use it as part of your `AppUnmanagedKinds` in your NewApp function (using the generated `operator` code from the CLI as a baseline):

```golang
package app

import (
	"github.com/grafana/grafana-app-sdk/app"
	"github.com/grafana/grafana-app-sdk/logging"
	"github.com/grafana/grafana-app-sdk/operator"
	"github.com/grafana/grafana-app-sdk/resource"
	"github.com/grafana/grafana-app-sdk/simple"
	
	// The other app's kinds
	otherappkind "<other-app>/<path-to-generated-code>" // example: github.com/grafana/grafana/apps/playlist/pkg/apis/playlist/v0alpha1

	generated "<my-app-repo>/pkg/generated"
)

func Provider(appCfg app.SpecificConfig) app.Provider {
	return simple.NewAppProvider(generated.LocalManifest(), appCfg, New)
}

func New(cfg app.Config) (app.App, error) {
	config := simple.AppConfig{
		Name:       "my-app",
		KubeConfig: cfg.KubeConfig,
		UnmanagedKinds: []simple.AppUnmanagedKind{{
			Kind: otherappkind.Kind(),
			Watcher: NewOtherAppKindWatcher(),
        }},
	}

	// Create the App
	a, err := simple.NewApp(config)
	if err != nil {
		return nil, err
	}

	// Validate the capabilities against the provided manifest to make sure there isn't a mismatch
	err = a.ValidateManifest(cfg.ManifestData)
	return a, err
}
```

Your watcher would then consume that imported other kind the same way you would write a watcher for a managed kind. 
Really, the only difference between using a kind managed and generated by your app vs a different app in this scenario 
is that you add an other app's kind to `UnmanagedKinds` instead of `ManagedKinds`. 
Your watcher would look essentially the same as a watcher for a managed kind:

```golang
package app

import (
	"context"
	"fmt"

	"github.com/grafana/grafana-app-sdk/operator"
	"github.com/grafana/grafana-app-sdk/resource"
	"github.com/grafana/grafana-app-sdk/simple"

	// The other app's kinds
	otherappkind "<other-app>/<path-to-generated-code>" // example: github.com/grafana/grafana/apps/playlist/pkg/apis/playlist/v0alpha1
)

func NewOtherKindWatcher() operator.ResourceWatcher {
	return simple.Watcher{
		AddFunc: func(ctx context.Context, obj resource.Object) error {
			cast, ok := obj.(*otherappkind.OtherType)
			if !ok {
				return fmt.Errorf("unable to cast object into *otherappkind.OtherType")
			}
			// Do something
			fmt.Println(cast.Spec.SomeField)
			return nil
		},
		// ...etc...
	}
}
```

## Using Another App's Kind in an Untyped Manner

If you don't have access to the other app's repository, or the other app doesn't have generated Kind information from 
the grafana-app-sdk, you can still use it in your app, just by either interacting with the kind in an untyped manner. 
To do this, you'll either use `resource.UntypedObject` (or `resource.UnstructuredWrapper`, they are very similar) or `resource.TypedSpecObject` (or `resource.TypedObject`). 
Here we'll cover using `resource.UntypedObject`, but the process is very similar for the various typed objects
(you'll just need to define the spec and other subresources in golang, rather than using an untyped map).

Since we can't just import the kind's package and use the `Kind()` function, we have to create the `resource.Kind` 
to use in our `UnmanagedKinds` ourselves. So let's do that:

```golang
package app

import (
	"github.com/grafana/grafana-app-sdk/app"
	"github.com/grafana/grafana-app-sdk/logging"
	"github.com/grafana/grafana-app-sdk/operator"
	"github.com/grafana/grafana-app-sdk/resource"
	"github.com/grafana/grafana-app-sdk/simple"

	generated "<my-app-repo>/pkg/generated"
)

var OtherAppKindUntyped = resource.Kind{
	Schema: resource.NewSimpleSchema("other-kind-group", "other-kind-version", &resource.UntypedObject{}, &resource.UntypedList{}, 
		resource.WithKind("OtherAppKind")), // Important! Make sure you add the WithKind here using the Kind's name, as otherwise it will default to the wrong name for the kind and be unable to look it up in the API server
}

func Provider(appCfg app.SpecificConfig) app.Provider {
	return simple.NewAppProvider(generated.LocalManifest(), appCfg, New)
}

func New(cfg app.Config) (app.App, error) {
	config := simple.AppConfig{
		Name:       "my-app",
		KubeConfig: cfg.KubeConfig,
		UnmanagedKinds: []simple.AppUnmanagedKind{{
			Kind: OtherAppKindUntyped,
			Watcher: NewOtherAppKindWatcher(),
        }},
	}

	// Create the App
	a, err := simple.NewApp(config)
	if err != nil {
		return nil, err
	}

	// Validate the capabilities against the provided manifest to make sure there isn't a mismatch
	err = a.ValidateManifest(cfg.ManifestData)
	return a, err
}
```

Now when we're interacting with the other kind via our watcher, the object we'll get is a type `*resource.UntypedKind`. 
This type exposes a `map[string]any` for its `Spec` type, but still has strongly-typed metadata 
(`resource.UnstructuredWrapper` doesn't break out the object into `Spec`/`ObjectMeta`/`TypeMeta`/`Subresources`, and just wraps the kubernetes `Unstrctured` type, which is a raw `map[string]any` for the whole object)

```golang
package app

import (
	"context"
	"fmt"

	"github.com/grafana/grafana-app-sdk/operator"
	"github.com/grafana/grafana-app-sdk/resource"
	"github.com/grafana/grafana-app-sdk/simple"
)

func NewOtherKindWatcher() operator.ResourceWatcher {
	return simple.Watcher{
		AddFunc: func(ctx context.Context, obj resource.Object) error {
			cast, ok := obj.(*resource.UntypedObject)
			if !ok {
				return fmt.Errorf("unable to cast object into *otherappkind.OtherType")
			}
			// Do something
			fmt.Println(cast.Spec["someField"])
			return nil
		},
		// ...etc...
	}
}
```

## Other Notes

### UnmanagedKinds with ManagedKinds

You can still use `ManagedKinds` alongside `UnmanagedKinds` when building your app, just don't have the same kind in both. 
It's important to not add an other app's kinds to your own `ManagedKinds` list, as then the App's `ManagedKinds()` method 
won't match up with the kinds declared in your app's Manifest, and `app.ValidateManifest` will fail.

### Opinionated Watchers/Reconcilers

By default, adding a Watcher or Reconciler for an unmanaged kind does not use the Opinionated variant of that 
Watcher/Reconciler. If you wish to use the Opinionated variant (same behavior as a Managed Kind), 
set `UseOpinionated` to `true` in the `AppUnamanagedKind.ReconcileOptions`. 

Please note that the Opinionated variants add finalizers to the watched resource as part of their function, 
so you will not only need appropriate permissions to update the Kind, but also as finalizers block deletion of the resource, 
if you ever _stop_ watching the Kind, you must remove all of your finalizers to allow the app that manages the Kind 
to fully process deletes. Before you do this, reach out to the team that maintains that Kind before 
using the opinionated variants on an Unmanaged Kind.

### Permissions

You'll need to make sure your app has permission to list and watch the other app's kind(s) you're using. 
You can add the list and watch permissions to you CUE manifest at:
```cue
manifest: extraPermissions: accessKinds: [{
	group: "other-app-group"
	resource: "other-app-kind-plural"
	actions: ["list","watch"]
}]
```

### Local Deployment

By default, the local environment set up by the CLI won't include the other app's kind(s). 
You'll have to add them by adding the CRD(s) for those kind(s) to `local/additional` as YAML file(s).